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"
22 #include "MetricsInfo.h"
23 #include "OutputParams.h"
24 #include "Paragraph.h"
25 #include "texstream.h"
26 #include "TocBackend.h"
28 #include "support/debug.h"
29 #include "support/gettext.h"
30 #include "support/lassert.h"
31 #include "support/lstrings.h"
32 #include "support/mutex.h"
34 #include "frontends/alert.h"
35 #include "frontends/FontMetrics.h"
36 #include "frontends/Painter.h"
44 using frontend::Painter;
45 using frontend::FontMetrics;
48 * Class Change has a changetime field that specifies the exact time at which
49 * a specific change was made. The change time is used as a guidance for the
50 * user while editing his document. Presently, it is not considered for LaTeX
52 * When merging two adjacent changes, the changetime is not considered,
53 * only the equality of the change type and author is checked (in method
54 * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
55 * the later change time is preserved.
58 bool Change::isSimilarTo(Change const & change) const
60 if (type != change.type)
63 if (type == Change::UNCHANGED)
66 return author == change.author;
70 Color Change::color() const
72 Color color = Color_none;
75 color = Color_addedtextauthor1;
78 color = Color_addedtextauthor2;
81 color = Color_addedtextauthor3;
84 color = Color_addedtextauthor4;
87 color = Color_addedtextauthor5;
92 color.mergeColor = Color_deletedtextmodifier;
98 bool operator==(Change const & l, Change const & r)
100 if (l.type != r.type)
103 // two changes of type UNCHANGED are always equal
104 if (l.type == Change::UNCHANGED)
107 return l.author == r.author && l.changetime == r.changetime;
111 bool operator!=(Change const & l, Change const & r)
117 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
119 return r1.start == r2.start && r1.end == r2.end;
123 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
129 bool Changes::Range::intersects(Range const & r) const
131 return r.start < end && r.end > start; // end itself is not in the range!
135 void Changes::set(Change const & change, pos_type const pos)
137 set(change, pos, pos + 1);
141 void Changes::set(Change const & change, pos_type const start, pos_type const end)
143 if (change.type != Change::UNCHANGED) {
144 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
145 << ", author: " << change.author
146 << ", time: " << long(change.changetime)
147 << ") in range (" << start << ", " << end << ")");
150 Range const newRange(start, end);
152 ChangeTable::iterator it = table_.begin();
154 for (; it != table_.end(); ) {
155 // current change starts like or follows new change
156 if (it->range.start >= start) {
160 // new change intersects with existing change
161 if (it->range.end > start) {
162 pos_type oldEnd = it->range.end;
163 it->range.end = start;
165 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
166 << " resulting in range (" << it->range.start << ", "
167 << it->range.end << ")");
171 LYXERR(Debug::CHANGES, " inserting tail in range ("
172 << end << ", " << oldEnd << ")");
173 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
181 if (change.type != Change::UNCHANGED) {
182 LYXERR(Debug::CHANGES, " inserting change");
183 it = table_.insert(it, ChangeRange(change, Range(start, end)));
187 for (; it != table_.end(); ) {
188 // new change 'contains' existing change
189 if (newRange.contains(it->range)) {
190 LYXERR(Debug::CHANGES, " removing subrange ("
191 << it->range.start << ", " << it->range.end << ")");
192 it = table_.erase(it);
196 // new change precedes existing change
197 if (it->range.start >= end)
200 // new change intersects with existing change
201 it->range.start = end;
202 LYXERR(Debug::CHANGES, " cutting head of type "
203 << it->change.type << " resulting in range ("
204 << end << ", " << it->range.end << ")");
205 break; // no need for another iteration
212 void Changes::erase(pos_type const pos)
214 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
216 for (ChangeRange & cr : table_) {
217 // range (pos,pos+x) becomes (pos,pos+x-1)
218 if (cr.range.start > pos)
220 // range (pos-x,pos) stays (pos-x,pos)
221 if (cr.range.end > pos)
229 void Changes::insert(Change const & change, lyx::pos_type pos)
231 if (change.type != Change::UNCHANGED) {
232 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
233 << " at position " << pos);
236 for (ChangeRange & cr : table_) {
237 // range (pos,pos+x) becomes (pos+1,pos+x+1)
238 if (cr.range.start >= pos)
241 // range (pos-x,pos) stays as it is
242 if (cr.range.end > pos)
246 set(change, pos, pos + 1); // set will call merge
250 Change const & Changes::lookup(pos_type const pos) const
252 static Change const noChange = Change(Change::UNCHANGED);
253 for (ChangeRange const & cr : table_)
254 if (cr.range.contains(pos))
260 bool Changes::isDeleted(pos_type start, pos_type end) const
262 for (ChangeRange const & cr : table_)
263 if (cr.range.contains(Range(start, end))) {
264 LYXERR(Debug::CHANGES, "range ("
265 << start << ", " << end << ") fully contains ("
266 << cr.range.start << ", " << cr.range.end
267 << ") of type " << cr.change.type);
268 return cr.change.type == Change::DELETED;
274 bool Changes::isChanged(pos_type const start, pos_type const end) const
276 for (ChangeRange const & cr : table_)
277 if (cr.range.intersects(Range(start, end))) {
278 LYXERR(Debug::CHANGES, "found intersection of range ("
279 << start << ", " << end << ") with ("
280 << cr.range.start << ", " << cr.range.end
281 << ") of type " << cr.change.type);
288 bool Changes::isChanged() const
290 for (ChangeRange const & cr : table_)
291 if (cr.change.changed())
297 void Changes::merge()
299 ChangeTable::iterator it = table_.begin();
301 while (it != table_.end()) {
302 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
303 << " and range (" << it->range.start << ", " << it->range.end
306 if (it->range.start == it->range.end) {
307 LYXERR(Debug::CHANGES, "removing empty range for pos "
316 if (it + 1 == table_.end())
319 if (it->change.isSimilarTo((it + 1)->change)
320 && it->range.end == (it + 1)->range.start) {
321 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
322 << it->range.end << ") and (" << (it + 1)->range.start << ", "
323 << (it + 1)->range.end << ")");
325 (it + 1)->range.start = it->range.start;
326 (it + 1)->change.changetime = max(it->change.changetime,
327 (it + 1)->change.changetime);
341 docstring getLaTeXMarkup(docstring const & macro, Author const & author,
342 docstring const & chgTime,
343 OutputParams const & runparams)
348 docstring uncodable_author;
349 odocstringstream ods;
351 docstring const author_name = author.name();
352 docstring const author_initials = author.initials();
355 if (!author_initials.empty()) {
356 docstring uncodable_initials;
357 // convert utf8 author initials to something representable
358 // in the current encoding
359 pair<docstring, docstring> author_initials_latexed =
360 runparams.encoding->latexString(author_initials, runparams.dryrun);
361 if (!author_initials_latexed.second.empty()) {
362 LYXERR0("Omitting uncodable characters '"
363 << author_initials_latexed.second
364 << "' in change author initials!");
365 uncodable_initials = author_initials;
367 ods << "[" << author_initials_latexed.first << "]";
368 // warn user (once) if we found uncodable glyphs.
369 if (!uncodable_initials.empty()) {
370 static std::set<docstring> warned_author_initials;
371 static Mutex warned_mutex;
372 Mutex::Locker locker(&warned_mutex);
373 if (warned_author_initials.find(uncodable_initials) == warned_author_initials.end()) {
374 frontend::Alert::warning(_("Uncodable character in author initials"),
375 support::bformat(_("The author initials '%1$s',\n"
376 "used for change tracking, contain the following glyphs that\n"
377 "cannot be represented in the current encoding: %2$s.\n"
378 "These glyphs will be omitted in the exported LaTeX file.\n\n"
379 "Choose an appropriate document encoding (such as utf8)\n"
380 "or change the author initials."),
381 uncodable_initials, author_initials_latexed.second));
382 warned_author_initials.insert(uncodable_initials);
386 // convert utf8 author name to something representable
387 // in the current encoding
388 pair<docstring, docstring> author_latexed =
389 runparams.encoding->latexString(author_name, runparams.dryrun);
390 if (!author_latexed.second.empty()) {
391 LYXERR0("Omitting uncodable characters '"
392 << author_latexed.second
393 << "' in change author name!");
394 uncodable_author = author_name;
396 ods << "{" << author_latexed.first << "}{" << chgTime << "}{";
398 // warn user (once) if we found uncodable glyphs.
399 if (!uncodable_author.empty()) {
400 static std::set<docstring> warned_authors;
401 static Mutex warned_mutex;
402 Mutex::Locker locker(&warned_mutex);
403 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
404 frontend::Alert::warning(_("Uncodable character in author name"),
405 support::bformat(_("The author name '%1$s',\n"
406 "used for change tracking, contains the following glyphs that\n"
407 "cannot be represented in the current encoding: %2$s.\n"
408 "These glyphs will be omitted in the exported LaTeX file.\n\n"
409 "Choose an appropriate document encoding (such as utf8)\n"
410 "or change the spelling of the author name."),
411 uncodable_author, author_latexed.second));
412 warned_authors.insert(uncodable_author);
422 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
423 Change const & oldChange, Change const & change,
424 OutputParams const & runparams)
426 if (!bparams.output_changes || oldChange == change)
431 if (oldChange.type != Change::UNCHANGED) {
432 if (oldChange.type != Change::DELETED || runparams.ctObject != OutputParams::CT_OMITOBJECT) {
433 // close \lyxadded or \lyxdeleted
437 if (oldChange.type == Change::DELETED
438 && !runparams.wasDisplayMath)
439 --runparams.inulemcmd;
443 chgTime += asctime(gmtime(&change.changetime));
444 // remove trailing '\n'
445 chgTime.erase(chgTime.end() - 1);
448 if (change.type == Change::DELETED) {
449 if (runparams.ctObject == OutputParams::CT_OMITOBJECT)
451 else if (runparams.ctObject == OutputParams::CT_OBJECT)
452 macro_beg = from_ascii("\\lyxobjdeleted");
453 else if (runparams.ctObject == OutputParams::CT_DISPLAYOBJECT)
454 macro_beg = from_ascii("\\lyxdisplayobjdeleted");
455 else if (runparams.ctObject == OutputParams::CT_UDISPLAYOBJECT)
456 macro_beg = from_ascii("\\lyxudisplayobjdeleted");
458 macro_beg = from_ascii("\\lyxdeleted");
459 if (!runparams.inDisplayMath)
460 ++runparams.inulemcmd;
463 else if (change.type == Change::INSERTED)
464 macro_beg = from_ascii("\\lyxadded");
466 docstring str = getLaTeXMarkup(macro_beg,
467 bparams.authors().get(change.author),
471 column += str.size();
477 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
478 Change const & old, Change const & change)
485 int const buffer_id = bparams.authors().get(change.author).bufferId();
487 switch (change.type) {
488 case Change::UNCHANGED:
489 os << "\n\\change_unchanged\n";
492 case Change::DELETED:
493 os << "\n\\change_deleted " << buffer_id
494 << " " << change.changetime << "\n";
497 case Change::INSERTED:
498 os << "\n\\change_inserted " << buffer_id
499 << " " << change.changetime << "\n";
505 void Changes::checkAuthors(AuthorList const & authorList) const
507 for (ChangeRange const & cr : table_)
508 if (cr.change.type != Change::UNCHANGED)
509 authorList.get(cr.change.author).setUsed(true);
513 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
514 bool output_active, TocBackend & backend) const
519 shared_ptr<Toc> change_list = backend.toc("change");
520 AuthorList const & author_list = buffer.params().authors();
521 DocIterator dit = cdit;
523 for (ChangeRange const & cr : table_) {
525 switch (cr.change.type) {
526 case Change::UNCHANGED:
528 case Change::DELETED:
529 // ✂ U+2702 BLACK SCISSORS
530 str.push_back(0x2702);
532 case Change::INSERTED:
533 // ✍ U+270D WRITING HAND
534 str.push_back(0x270d);
537 dit.pos() = cr.range.start;
538 Paragraph const & par = dit.paragraph();
539 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
540 if (cr.range.end > par.size())
541 // ¶ U+00B6 PILCROW SIGN
543 docstring const & author = author_list.get(cr.change.author).name();
544 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
545 if (it == change_list->end()) {
546 change_list->push_back(TocItem(dit, 0, author, true));
547 change_list->push_back(TocItem(dit, 1, str, output_active));
550 for (++it; it != change_list->end(); ++it) {
551 if (it->depth() == 0 && it->str() != author)
554 change_list->insert(it, TocItem(dit, 1, str, output_active));
559 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
560 double const x2, FontInfo const & font) const
562 if (!changed() || (!lyxrc.ct_additions_underlined && inserted()))
564 // Calculate 1/3 height of font
565 FontMetrics const & fm = theFontMetrics(font);
566 double const y_bar = deleted() ? y - fm.maxAscent() / 3
567 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
568 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
569 Painter::line_solid, pi.base.solidLineThickness());
573 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
574 double const x2, double const y2) const
589 if (!lyxrc.ct_additions_underlined)
591 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
592 color(), Painter::line_solid,
593 pi.base.solidLineThickness());
597 // FIXME: we cannot use antialias since we keep drawing on the same
598 // background with the current painting mechanism.
599 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
600 color(), Painter::line_solid_aliased,
601 pi.base.solidLineThickness());