3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
9 * Full author contact details are available in file CREDITS.
11 * Record changes in a paragraph.
19 #include "BufferParams.h"
23 #include "MetricsInfo.h"
24 #include "OutputParams.h"
25 #include "Paragraph.h"
26 #include "texstream.h"
27 #include "TocBackend.h"
29 #include "support/debug.h"
30 #include "support/gettext.h"
31 #include "support/lassert.h"
32 #include "support/lstrings.h"
33 #include "support/mutex.h"
35 #include "frontends/alert.h"
36 #include "frontends/FontMetrics.h"
37 #include "frontends/Painter.h"
45 using frontend::Painter;
46 using frontend::FontMetrics;
49 * Class Change has a changetime field that specifies the exact time at which
50 * a specific change was made. The change time is used as a guidance for the
51 * user while editing his document. Presently, it is not considered for LaTeX
53 * When merging two adjacent changes, the changetime is not considered,
54 * only the equality of the change type and author is checked (in method
55 * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
56 * the later change time is preserved.
59 bool Change::isSimilarTo(Change const & change) const
61 if (type != change.type)
64 if (type == Change::UNCHANGED)
67 return author == change.author;
71 Color Change::color() const
73 Color color = Color_none;
76 color = Color_addedtextauthor1;
79 color = Color_addedtextauthor2;
82 color = Color_addedtextauthor3;
85 color = Color_addedtextauthor4;
88 color = Color_addedtextauthor5;
93 color.mergeColor = Color_deletedtextmodifier;
99 bool operator==(Change const & l, Change const & r)
101 if (l.type != r.type)
104 // two changes of type UNCHANGED are always equal
105 if (l.type == Change::UNCHANGED)
108 return l.author == r.author && l.changetime == r.changetime;
112 bool operator!=(Change const & l, Change const & r)
118 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
120 return r1.start == r2.start && r1.end == r2.end;
124 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
130 bool Changes::Range::intersects(Range const & r) const
132 return r.start < end && r.end > start; // end itself is not in the range!
136 void Changes::set(Change const & change, pos_type const pos)
138 set(change, pos, pos + 1);
142 void Changes::set(Change const & change, pos_type const start, pos_type const end)
144 if (change.type != Change::UNCHANGED) {
145 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
146 << ", author: " << change.author
147 << ", time: " << long(change.changetime)
148 << ") in range (" << start << ", " << end << ")");
151 Range const newRange(start, end);
153 ChangeTable::iterator it = table_.begin();
155 for (; it != table_.end(); ) {
156 // current change starts like or follows new change
157 if (it->range.start >= start) {
161 // new change intersects with existing change
162 if (it->range.end > start) {
163 pos_type oldEnd = it->range.end;
164 it->range.end = start;
166 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
167 << " resulting in range (" << it->range.start << ", "
168 << it->range.end << ")");
172 LYXERR(Debug::CHANGES, " inserting tail in range ("
173 << end << ", " << oldEnd << ")");
174 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
182 if (change.type != Change::UNCHANGED) {
183 LYXERR(Debug::CHANGES, " inserting change");
184 it = table_.insert(it, ChangeRange(change, Range(start, end)));
188 for (; it != table_.end(); ) {
189 // new change 'contains' existing change
190 if (newRange.contains(it->range)) {
191 LYXERR(Debug::CHANGES, " removing subrange ("
192 << it->range.start << ", " << it->range.end << ")");
193 it = table_.erase(it);
197 // new change precedes existing change
198 if (it->range.start >= end)
201 // new change intersects with existing change
202 it->range.start = end;
203 LYXERR(Debug::CHANGES, " cutting head of type "
204 << it->change.type << " resulting in range ("
205 << end << ", " << it->range.end << ")");
206 break; // no need for another iteration
213 void Changes::erase(pos_type const pos)
215 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
217 for (ChangeRange & cr : table_) {
218 // range (pos,pos+x) becomes (pos,pos+x-1)
219 if (cr.range.start > pos)
221 // range (pos-x,pos) stays (pos-x,pos)
222 if (cr.range.end > pos)
230 void Changes::insert(Change const & change, lyx::pos_type pos)
232 if (change.type != Change::UNCHANGED) {
233 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
234 << " at position " << pos);
237 for (ChangeRange & cr : table_) {
238 // range (pos,pos+x) becomes (pos+1,pos+x+1)
239 if (cr.range.start >= pos)
242 // range (pos-x,pos) stays as it is
243 if (cr.range.end > pos)
247 set(change, pos, pos + 1); // set will call merge
251 Change const & Changes::lookup(pos_type const pos) const
253 static Change const noChange = Change(Change::UNCHANGED);
254 for (ChangeRange const & cr : table_)
255 if (cr.range.contains(pos))
261 bool Changes::isDeleted(pos_type start, pos_type end) const
263 for (ChangeRange const & cr : table_)
264 if (cr.range.contains(Range(start, end))) {
265 LYXERR(Debug::CHANGES, "range ("
266 << start << ", " << end << ") fully contains ("
267 << cr.range.start << ", " << cr.range.end
268 << ") of type " << cr.change.type);
269 return cr.change.type == Change::DELETED;
275 bool Changes::isChanged(pos_type const start, pos_type const end) const
277 for (ChangeRange const & cr : table_)
278 if (cr.range.intersects(Range(start, end))) {
279 LYXERR(Debug::CHANGES, "found intersection of range ("
280 << start << ", " << end << ") with ("
281 << cr.range.start << ", " << cr.range.end
282 << ") of type " << cr.change.type);
289 bool Changes::isChanged() const
291 for (ChangeRange const & cr : table_)
292 if (cr.change.changed())
298 void Changes::merge()
300 ChangeTable::iterator it = table_.begin();
302 while (it != table_.end()) {
303 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
304 << " and range (" << it->range.start << ", " << it->range.end
307 if (it->range.start == it->range.end) {
308 LYXERR(Debug::CHANGES, "removing empty range for pos "
317 if (it + 1 == table_.end())
320 if (it->change.isSimilarTo((it + 1)->change)
321 && it->range.end == (it + 1)->range.start) {
322 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
323 << it->range.end << ") and (" << (it + 1)->range.start << ", "
324 << (it + 1)->range.end << ")");
326 (it + 1)->range.start = it->range.start;
327 (it + 1)->change.changetime = max(it->change.changetime,
328 (it + 1)->change.changetime);
342 docstring getLaTeXMarkup(docstring const & macro, Author const & author,
343 docstring const & chgTime,
344 OutputParams const & runparams)
349 docstring uncodable_author;
350 odocstringstream ods;
352 docstring const author_name = author.name();
353 docstring const author_initials = author.initials();
356 if (!author_initials.empty()) {
357 docstring uncodable_initials;
358 // convert utf8 author initials to something representable
359 // in the current encoding
360 pair<docstring, docstring> author_initials_latexed =
361 runparams.encoding->latexString(author_initials, runparams.dryrun);
362 if (!author_initials_latexed.second.empty()) {
363 LYXERR0("Omitting uncodable characters '"
364 << author_initials_latexed.second
365 << "' in change author initials!");
366 uncodable_initials = author_initials;
368 ods << "[" << author_initials_latexed.first << "]";
369 // warn user (once) if we found uncodable glyphs.
370 if (!uncodable_initials.empty()) {
371 static std::set<docstring> warned_author_initials;
372 static Mutex warned_mutex;
373 Mutex::Locker locker(&warned_mutex);
374 if (warned_author_initials.find(uncodable_initials) == warned_author_initials.end()) {
375 frontend::Alert::warning(_("Uncodable character in author initials"),
376 support::bformat(_("The author initials '%1$s',\n"
377 "used for change tracking, contain the following glyphs that\n"
378 "cannot be represented in the current encoding: %2$s.\n"
379 "These glyphs will be omitted in the exported LaTeX file.\n\n"
380 "Choose an appropriate document encoding (such as utf8)\n"
381 "or change the author initials."),
382 uncodable_initials, author_initials_latexed.second));
383 warned_author_initials.insert(uncodable_initials);
387 // convert utf8 author name to something representable
388 // in the current encoding
389 pair<docstring, docstring> author_latexed =
390 runparams.encoding->latexString(author_name, runparams.dryrun);
391 if (!author_latexed.second.empty()) {
392 LYXERR0("Omitting uncodable characters '"
393 << author_latexed.second
394 << "' in change author name!");
395 uncodable_author = author_name;
397 ods << "{" << author_latexed.first << "}{" << chgTime << "}{";
399 // warn user (once) if we found uncodable glyphs.
400 if (!uncodable_author.empty()) {
401 static std::set<docstring> warned_authors;
402 static Mutex warned_mutex;
403 Mutex::Locker locker(&warned_mutex);
404 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
405 frontend::Alert::warning(_("Uncodable character in author name"),
406 support::bformat(_("The author name '%1$s',\n"
407 "used for change tracking, contains the following glyphs that\n"
408 "cannot be represented in the current encoding: %2$s.\n"
409 "These glyphs will be omitted in the exported LaTeX file.\n\n"
410 "Choose an appropriate document encoding (such as utf8)\n"
411 "or change the spelling of the author name."),
412 uncodable_author, author_latexed.second));
413 warned_authors.insert(uncodable_author);
423 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
424 Change const & oldChange, Change const & change,
425 OutputParams const & runparams)
427 if (!bparams.output_changes || oldChange == change)
432 if (oldChange.type != Change::UNCHANGED) {
433 if (oldChange.type != Change::DELETED || runparams.ctObject != CtObject::OmitObject) {
434 // close \lyxadded or \lyxdeleted
438 if (oldChange.type == Change::DELETED
439 && !runparams.wasDisplayMath)
440 --runparams.inulemcmd;
444 chgTime += asctime(gmtime(&change.changetime));
445 // remove trailing '\n'
446 chgTime.erase(chgTime.end() - 1);
449 if (change.type == Change::DELETED) {
450 if (runparams.ctObject == CtObject::OmitObject)
452 else if (runparams.ctObject == CtObject::Object)
453 macro_beg = from_ascii("\\lyxobjdeleted");
454 else if (runparams.ctObject == CtObject::DisplayObject)
455 macro_beg = from_ascii("\\lyxdisplayobjdeleted");
456 else if (runparams.ctObject == CtObject::UDisplayObject)
457 macro_beg = from_ascii("\\lyxudisplayobjdeleted");
459 macro_beg = from_ascii("\\lyxdeleted");
460 if (!runparams.inDisplayMath)
461 ++runparams.inulemcmd;
464 else if (change.type == Change::INSERTED)
465 macro_beg = from_ascii("\\lyxadded");
467 docstring str = getLaTeXMarkup(macro_beg,
468 bparams.authors().get(change.author),
472 column += str.size();
478 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
479 Change const & old, Change const & change)
486 int const buffer_id = bparams.authors().get(change.author).bufferId();
488 switch (change.type) {
489 case Change::UNCHANGED:
490 os << "\n\\change_unchanged\n";
493 case Change::DELETED:
494 os << "\n\\change_deleted " << buffer_id
495 << " " << change.changetime << "\n";
498 case Change::INSERTED:
499 os << "\n\\change_inserted " << buffer_id
500 << " " << change.changetime << "\n";
506 void Changes::checkAuthors(AuthorList const & authorList) const
508 for (ChangeRange const & cr : table_)
509 if (cr.change.type != Change::UNCHANGED)
510 authorList.get(cr.change.author).setUsed(true);
514 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
515 bool output_active, TocBackend & backend) const
520 shared_ptr<Toc> change_list = backend.toc("change");
521 AuthorList const & author_list = buffer.params().authors();
522 DocIterator dit = cdit;
524 for (ChangeRange const & cr : table_) {
526 switch (cr.change.type) {
527 case Change::UNCHANGED:
529 case Change::DELETED:
530 // ✂ U+2702 BLACK SCISSORS
531 str.push_back(0x2702);
533 case Change::INSERTED:
534 // ✍ U+270D WRITING HAND
535 str.push_back(0x270d);
538 dit.pos() = cr.range.start;
539 Paragraph const & par = dit.paragraph();
540 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
541 if (cr.range.end > par.size())
542 // ¶ U+00B6 PILCROW SIGN
544 docstring const & author = author_list.get(cr.change.author).name();
545 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
546 if (it == change_list->end()) {
547 change_list->push_back(TocItem(dit, 0, author, true));
548 change_list->push_back(TocItem(dit, 1, str, output_active));
551 for (++it; it != change_list->end(); ++it) {
552 if (it->depth() == 0 && it->str() != author)
555 change_list->insert(it, TocItem(dit, 1, str, output_active));
560 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
561 double const x2, FontInfo const & font) const
563 if (!changed() || (!lyxrc.ct_additions_underlined && inserted()))
565 // Calculate 1/3 height of font
566 FontMetrics const & fm = theFontMetrics(font);
567 double const y_bar = deleted() ? y - fm.maxAscent() / 3
568 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
569 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
570 Painter::line_solid, pi.base.solidLineThickness());
574 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
575 double const x2, double const y2) const
590 if (!lyxrc.ct_additions_underlined)
592 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
593 color(), Painter::line_solid,
594 pi.base.solidLineThickness());
598 // FIXME: we cannot use antialias since we keep drawing on the same
599 // background with the current painting mechanism.
600 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
601 color(), Painter::line_solid_aliased,
602 pi.base.solidLineThickness());