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"
21 #include "LaTeXFeatures.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_author);
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 bool const dvipost = LaTeXFeatures::isAvailable("dvipost") &&
432 (runparams.flavor == OutputParams::LATEX
433 || runparams.flavor == OutputParams::DVILUATEX);
435 if (oldChange.type != Change::UNCHANGED) {
436 // close \lyxadded or \lyxdeleted
439 if (oldChange.type == Change::DELETED
440 && !runparams.wasDisplayMath && !dvipost)
441 --runparams.inulemcmd;
445 chgTime += asctime(gmtime(&change.changetime));
446 // remove trailing '\n'
447 chgTime.erase(chgTime.end() - 1);
450 if (change.type == Change::DELETED) {
451 macro_beg = from_ascii("\\lyxdeleted");
452 if (!runparams.inDisplayMath && !dvipost)
453 ++runparams.inulemcmd;
455 else if (change.type == Change::INSERTED)
456 macro_beg = from_ascii("\\lyxadded");
458 docstring str = getLaTeXMarkup(macro_beg,
459 bparams.authors().get(change.author),
462 // signature needed by \lyxsout to correctly strike out display math
463 if (change.type == Change::DELETED && runparams.inDisplayMath
466 str += from_ascii("\\\\\\noindent\n");
468 str += from_ascii("\\\\\\\\\n");
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)
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 Changes::updateBuffer(Buffer const & buf)
562 bool const changed = isChanged();
563 buf.setChangesPresent(buf.areChangesPresent() || changed);
564 previously_changed_ = changed;
568 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
569 double const x2, FontInfo const & font) const
573 // Calculate 1/3 height of font
574 FontMetrics const & fm = theFontMetrics(font);
575 double const y_bar = deleted() ? y - fm.maxAscent() / 3
576 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
577 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
578 Painter::line_solid, pi.base.solidLineThickness());
582 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
583 double const x2, double const y2) const
598 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
599 color(), Painter::line_solid,
600 pi.base.solidLineThickness());
603 // FIXME: we cannot use antialias since we keep drawing on the same
604 // background with the current painting mechanism.
605 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
606 color(), Painter::line_solid_aliased,
607 pi.base.solidLineThickness());