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;
75 color = Color_changedtext_workarea_author1;
77 color = Color_changedtext_workarea_comparison;
79 switch ((author - 2) % 4) {
81 color = Color_changedtext_workarea_author2;
84 color = Color_changedtext_workarea_author3;
87 color = Color_changedtext_workarea_author4;
90 color = Color_changedtext_workarea_author5;
96 color.mergeColor = Color_deletedtext_workarea_modifier;
102 bool operator==(Change const & l, Change const & r)
104 if (l.type != r.type)
107 // two changes of type UNCHANGED are always equal
108 if (l.type == Change::UNCHANGED)
111 return l.author == r.author && l.changetime == r.changetime;
115 bool operator!=(Change const & l, Change const & r)
121 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
123 return r1.start == r2.start && r1.end == r2.end;
127 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
133 bool Changes::Range::intersects(Range const & r) const
135 return r.start < end && r.end > start; // end itself is not in the range!
139 void Changes::set(Change const & change, pos_type const pos)
141 set(change, pos, pos + 1);
145 void Changes::set(Change const & change, pos_type const start, pos_type const end)
147 if (change.type != Change::UNCHANGED) {
148 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
149 << ", author: " << change.author
150 << ", time: " << long(change.changetime)
151 << ") in range (" << start << ", " << end << ")");
154 Range const newRange(start, end);
156 ChangeTable::iterator it = table_.begin();
158 for (; it != table_.end(); ) {
159 // current change starts like or follows new change
160 if (it->range.start >= start) {
164 // new change intersects with existing change
165 if (it->range.end > start) {
166 pos_type oldEnd = it->range.end;
167 it->range.end = start;
169 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
170 << " resulting in range (" << it->range.start << ", "
171 << it->range.end << ")");
175 LYXERR(Debug::CHANGES, " inserting tail in range ("
176 << end << ", " << oldEnd << ")");
177 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
185 if (change.type != Change::UNCHANGED) {
186 LYXERR(Debug::CHANGES, " inserting change");
187 it = table_.insert(it, ChangeRange(change, Range(start, end)));
191 for (; it != table_.end(); ) {
192 // new change 'contains' existing change
193 if (newRange.contains(it->range)) {
194 LYXERR(Debug::CHANGES, " removing subrange ("
195 << it->range.start << ", " << it->range.end << ")");
196 it = table_.erase(it);
200 // new change precedes existing change
201 if (it->range.start >= end)
204 // new change intersects with existing change
205 it->range.start = end;
206 LYXERR(Debug::CHANGES, " cutting head of type "
207 << it->change.type << " resulting in range ("
208 << end << ", " << it->range.end << ")");
209 break; // no need for another iteration
216 void Changes::erase(pos_type const pos)
218 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
220 for (ChangeRange & cr : table_) {
221 // range (pos,pos+x) becomes (pos,pos+x-1)
222 if (cr.range.start > pos)
224 // range (pos-x,pos) stays (pos-x,pos)
225 if (cr.range.end > pos)
233 void Changes::insert(Change const & change, lyx::pos_type pos)
235 if (change.type != Change::UNCHANGED) {
236 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
237 << " at position " << pos);
240 for (ChangeRange & cr : table_) {
241 // range (pos,pos+x) becomes (pos+1,pos+x+1)
242 if (cr.range.start >= pos)
245 // range (pos-x,pos) stays as it is
246 if (cr.range.end > pos)
250 set(change, pos, pos + 1); // set will call merge
254 Change const & Changes::lookup(pos_type const pos) const
256 static Change const noChange = Change(Change::UNCHANGED);
257 for (ChangeRange const & cr : table_)
258 if (cr.range.contains(pos))
264 bool Changes::isDeleted(pos_type start, pos_type end) const
266 for (ChangeRange const & cr : table_)
267 if (cr.range.contains(Range(start, end))) {
268 LYXERR(Debug::CHANGES, "range ("
269 << start << ", " << end << ") fully contains ("
270 << cr.range.start << ", " << cr.range.end
271 << ") of type " << cr.change.type);
272 return cr.change.type == Change::DELETED;
278 bool Changes::isChanged(pos_type const start, pos_type const end) const
280 for (ChangeRange const & cr : table_)
281 if (cr.range.intersects(Range(start, end))) {
282 LYXERR(Debug::CHANGES, "found intersection of range ("
283 << start << ", " << end << ") with ("
284 << cr.range.start << ", " << cr.range.end
285 << ") of type " << cr.change.type);
292 bool Changes::isChanged() const
294 for (ChangeRange const & cr : table_)
295 if (cr.change.changed())
301 void Changes::merge()
303 ChangeTable::iterator it = table_.begin();
305 while (it != table_.end()) {
306 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
307 << " and range (" << it->range.start << ", " << it->range.end
310 if (it->range.start == it->range.end) {
311 LYXERR(Debug::CHANGES, "removing empty range for pos "
320 if (it + 1 == table_.end())
323 if (it->change.isSimilarTo((it + 1)->change)
324 && it->range.end == (it + 1)->range.start) {
325 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
326 << it->range.end << ") and (" << (it + 1)->range.start << ", "
327 << (it + 1)->range.end << ")");
329 (it + 1)->range.start = it->range.start;
330 (it + 1)->change.changetime = max(it->change.changetime,
331 (it + 1)->change.changetime);
345 docstring getLaTeXMarkup(docstring const & macro, Author const & author,
346 docstring const & chgTime,
347 OutputParams const & runparams)
352 docstring uncodable_author;
353 odocstringstream ods;
355 docstring const author_name = author.name();
356 docstring const author_initials = author.initials();
359 if (!author_initials.empty()) {
360 docstring uncodable_initials;
361 // convert utf8 author initials to something representable
362 // in the current encoding
363 pair<docstring, docstring> author_initials_latexed =
364 runparams.encoding->latexString(author_initials, runparams.dryrun);
365 if (!author_initials_latexed.second.empty()) {
366 LYXERR0("Omitting uncodable characters '"
367 << author_initials_latexed.second
368 << "' in change author initials!");
369 uncodable_initials = author_initials;
371 ods << "[" << author_initials_latexed.first << "]";
372 // warn user (once) if we found uncodable glyphs.
373 if (!uncodable_initials.empty()) {
374 static std::set<docstring> warned_author_initials;
375 static Mutex warned_mutex;
376 Mutex::Locker locker(&warned_mutex);
377 if (warned_author_initials.find(uncodable_initials) == warned_author_initials.end()) {
378 frontend::Alert::warning(_("Uncodable character in author initials"),
379 support::bformat(_("The author initials '%1$s',\n"
380 "used for change tracking, contain the following glyphs that\n"
381 "cannot be represented in the current encoding: %2$s.\n"
382 "These glyphs will be omitted in the exported LaTeX file.\n\n"
383 "Choose an appropriate document encoding (such as utf8)\n"
384 "or change the author initials."),
385 uncodable_initials, author_initials_latexed.second));
386 warned_author_initials.insert(uncodable_initials);
390 // convert utf8 author name to something representable
391 // in the current encoding
392 pair<docstring, docstring> author_latexed =
393 runparams.encoding->latexString(author_name, runparams.dryrun);
394 if (!author_latexed.second.empty()) {
395 LYXERR0("Omitting uncodable characters '"
396 << author_latexed.second
397 << "' in change author name!");
398 uncodable_author = author_name;
400 ods << "{" << author_latexed.first << "}{" << chgTime << "}{";
402 // warn user (once) if we found uncodable glyphs.
403 if (!uncodable_author.empty()) {
404 static std::set<docstring> warned_authors;
405 static Mutex warned_mutex;
406 Mutex::Locker locker(&warned_mutex);
407 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
408 frontend::Alert::warning(_("Uncodable character in author name"),
409 support::bformat(_("The author name '%1$s',\n"
410 "used for change tracking, contains the following glyphs that\n"
411 "cannot be represented in the current encoding: %2$s.\n"
412 "These glyphs will be omitted in the exported LaTeX file.\n\n"
413 "Choose an appropriate document encoding (such as utf8)\n"
414 "or change the spelling of the author name."),
415 uncodable_author, author_latexed.second));
416 warned_authors.insert(uncodable_author);
426 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
427 Change const & oldChange, Change const & change,
428 OutputParams const & runparams)
430 if (!bparams.output_changes || oldChange == change)
435 if (oldChange.type != Change::UNCHANGED) {
436 if (oldChange.type != Change::DELETED || runparams.ctObject != CtObject::OmitObject) {
437 // close \lyxadded or \lyxdeleted
441 if (oldChange.type == Change::DELETED
442 && !runparams.wasDisplayMath)
443 --runparams.inulemcmd;
447 chgTime += asctime(gmtime(&change.changetime));
448 // remove trailing '\n'
449 chgTime.erase(chgTime.end() - 1);
452 if (change.type == Change::DELETED) {
453 if (runparams.ctObject == CtObject::OmitObject)
455 else if (runparams.ctObject == CtObject::Object)
456 macro_beg = from_ascii("\\lyxobjdeleted");
457 else if (runparams.ctObject == CtObject::DisplayObject)
458 macro_beg = from_ascii("\\lyxdisplayobjdeleted");
459 else if (runparams.ctObject == CtObject::UDisplayObject)
460 macro_beg = from_ascii("\\lyxudisplayobjdeleted");
462 macro_beg = from_ascii("\\lyxdeleted");
463 if (!runparams.inDisplayMath)
464 ++runparams.inulemcmd;
467 else if (change.type == Change::INSERTED)
468 macro_beg = from_ascii("\\lyxadded");
470 docstring str = getLaTeXMarkup(macro_beg,
471 bparams.authors().get(change.author),
475 column += str.size();
481 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
482 Change const & old, Change const & change)
489 int const buffer_id = bparams.authors().get(change.author).bufferId();
491 switch (change.type) {
492 case Change::UNCHANGED:
493 os << "\n\\change_unchanged\n";
496 case Change::DELETED:
497 os << "\n\\change_deleted " << buffer_id
498 << " " << change.changetime << "\n";
501 case Change::INSERTED:
502 os << "\n\\change_inserted " << buffer_id
503 << " " << change.changetime << "\n";
509 void Changes::checkAuthors(AuthorList const & authorList) const
511 for (ChangeRange const & cr : table_)
512 if (cr.change.type != Change::UNCHANGED)
513 authorList.get(cr.change.author).setUsed(true);
517 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
518 bool output_active, TocBackend & backend) const
523 shared_ptr<Toc> change_list = backend.toc("change");
524 AuthorList const & author_list = buffer.params().authors();
525 DocIterator dit = cdit;
527 for (ChangeRange const & cr : table_) {
529 switch (cr.change.type) {
530 case Change::UNCHANGED:
532 case Change::DELETED:
533 // ✂ U+2702 BLACK SCISSORS
534 str.push_back(0x2702);
536 case Change::INSERTED:
537 // ✍ U+270D WRITING HAND
538 str.push_back(0x270d);
541 dit.pos() = cr.range.start;
542 Paragraph const & par = dit.paragraph();
543 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
544 if (cr.range.end > par.size())
545 // ¶ U+00B6 PILCROW SIGN
547 docstring const & author = author_list.get(cr.change.author).name();
548 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
549 if (it == change_list->end()) {
550 change_list->push_back(TocItem(dit, 0, author, true));
551 change_list->push_back(TocItem(dit, 1, str, output_active));
554 for (++it; it != change_list->end(); ++it) {
555 if (it->depth() == 0 && it->str() != author)
558 change_list->insert(it, TocItem(dit, 1, str, output_active));
563 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
564 double const x2, FontInfo const & font) const
566 if (!changed() || (!lyxrc.ct_additions_underlined && inserted()))
568 // Calculate 1/3 height of font
569 FontMetrics const & fm = theFontMetrics(font);
570 double const y_bar = deleted() ? y - fm.maxAscent() / 3
571 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
572 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
573 Painter::line_solid, pi.base.solidLineThickness());
577 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
578 double const x2, double const y2) const
593 if (!lyxrc.ct_additions_underlined)
595 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
596 color(), Painter::line_solid,
597 pi.base.solidLineThickness());
601 // FIXME: we cannot use antialias since we keep drawing on the same
602 // background with the current painting mechanism.
603 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
604 color(), Painter::line_solid_aliased,
605 pi.base.solidLineThickness());