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_changedtextauthor1;
78 color = Color_changedtextauthor2;
81 color = Color_changedtextauthor3;
84 color = Color_changedtextauthor4;
87 color = Color_changedtextauthor5;
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 << ")");
149 is_update_required_ = true;
152 Range const newRange(start, end);
154 ChangeTable::iterator it = table_.begin();
156 for (; it != table_.end(); ) {
157 // current change starts like or follows new change
158 if (it->range.start >= start) {
162 // new change intersects with existing change
163 if (it->range.end > start) {
164 pos_type oldEnd = it->range.end;
165 it->range.end = start;
167 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
168 << " resulting in range (" << it->range.start << ", "
169 << it->range.end << ")");
173 LYXERR(Debug::CHANGES, " inserting tail in range ("
174 << end << ", " << oldEnd << ")");
175 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
183 if (change.type != Change::UNCHANGED) {
184 LYXERR(Debug::CHANGES, " inserting change");
185 it = table_.insert(it, ChangeRange(change, Range(start, end)));
189 for (; it != table_.end(); ) {
190 // new change 'contains' existing change
191 if (newRange.contains(it->range)) {
192 LYXERR(Debug::CHANGES, " removing subrange ("
193 << it->range.start << ", " << it->range.end << ")");
194 it = table_.erase(it);
198 // new change precedes existing change
199 if (it->range.start >= end)
202 // new change intersects with existing change
203 it->range.start = end;
204 LYXERR(Debug::CHANGES, " cutting head of type "
205 << it->change.type << " resulting in range ("
206 << end << ", " << it->range.end << ")");
207 break; // no need for another iteration
214 void Changes::erase(pos_type const pos)
216 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
218 ChangeTable::iterator it = table_.begin();
219 ChangeTable::iterator end = table_.end();
221 for (; it != end; ++it) {
222 // range (pos,pos+x) becomes (pos,pos+x-1)
223 if (it->range.start > pos)
225 // range (pos-x,pos) stays (pos-x,pos)
226 if (it->range.end > pos)
234 void Changes::insert(Change const & change, lyx::pos_type pos)
236 if (change.type != Change::UNCHANGED) {
237 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
238 << " at position " << pos);
241 ChangeTable::iterator it = table_.begin();
242 ChangeTable::iterator end = table_.end();
244 for (; it != end; ++it) {
245 // range (pos,pos+x) becomes (pos+1,pos+x+1)
246 if (it->range.start >= pos)
249 // range (pos-x,pos) stays as it is
250 if (it->range.end > pos)
254 set(change, pos, pos + 1); // set will call merge
258 Change const & Changes::lookup(pos_type const pos) const
260 static Change const noChange = Change(Change::UNCHANGED);
262 ChangeTable::const_iterator it = table_.begin();
263 ChangeTable::const_iterator const end = table_.end();
265 for (; it != end; ++it) {
266 if (it->range.contains(pos))
274 bool Changes::isDeleted(pos_type start, pos_type end) const
276 ChangeTable::const_iterator it = table_.begin();
277 ChangeTable::const_iterator const itend = table_.end();
279 for (; it != itend; ++it) {
280 if (it->range.contains(Range(start, end))) {
281 LYXERR(Debug::CHANGES, "range ("
282 << start << ", " << end << ") fully contains ("
283 << it->range.start << ", " << it->range.end
284 << ") of type " << it->change.type);
285 return it->change.type == Change::DELETED;
292 bool Changes::isChanged(pos_type const start, pos_type const end) const
294 ChangeTable::const_iterator it = table_.begin();
295 ChangeTable::const_iterator const itend = table_.end();
297 for (; it != itend; ++it) {
298 if (it->range.intersects(Range(start, end))) {
299 LYXERR(Debug::CHANGES, "found intersection of range ("
300 << start << ", " << end << ") with ("
301 << it->range.start << ", " << it->range.end
302 << ") of type " << it->change.type);
310 bool Changes::isChanged() const
312 ChangeTable::const_iterator it = table_.begin();
313 ChangeTable::const_iterator const itend = table_.end();
314 for (; it != itend; ++it) {
315 if (it->change.changed())
322 void Changes::merge()
325 ChangeTable::iterator it = table_.begin();
327 while (it != table_.end()) {
328 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
329 << " and range (" << it->range.start << ", " << it->range.end
332 if (it->range.start == it->range.end) {
333 LYXERR(Debug::CHANGES, "removing empty range for pos "
343 if (it + 1 == table_.end())
346 if (it->change.isSimilarTo((it + 1)->change)
347 && it->range.end == (it + 1)->range.start) {
348 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
349 << it->range.end << ") and (" << (it + 1)->range.start << ", "
350 << (it + 1)->range.end << ")");
352 (it + 1)->range.start = it->range.start;
353 (it + 1)->change.changetime = max(it->change.changetime,
354 (it + 1)->change.changetime);
364 if (merged && !isChanged())
365 is_update_required_ = true;
371 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
372 docstring const & chgTime,
373 OutputParams const & runparams)
378 docstring uncodable_author;
379 odocstringstream ods;
382 // convert utf8 author name to something representable
383 // in the current encoding
384 pair<docstring, docstring> author_latexed =
385 runparams.encoding->latexString(author, runparams.dryrun);
386 if (!author_latexed.second.empty()) {
387 LYXERR0("Omitting uncodable characters '"
388 << author_latexed.second
389 << "' in change author name!");
390 uncodable_author = author;
392 ods << author_latexed.first << "}{" << chgTime << "}{";
394 // warn user (once) if we found uncodable glyphs.
395 if (!uncodable_author.empty()) {
396 static std::set<docstring> warned_authors;
397 static Mutex warned_mutex;
398 Mutex::Locker locker(&warned_mutex);
399 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
400 frontend::Alert::warning(_("Uncodable character in author name"),
401 support::bformat(_("The author name '%1$s',\n"
402 "used for change tracking, contains the following glyphs that\n"
403 "cannot be represented in the current encoding: %2$s.\n"
404 "These glyphs will be omitted in the exported LaTeX file.\n\n"
405 "Choose an appropriate document encoding (such as utf8)\n"
406 "or change the spelling of the author name."),
407 uncodable_author, author_latexed.second));
408 warned_authors.insert(uncodable_author);
418 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
419 Change const & oldChange, Change const & change,
420 OutputParams const & runparams)
422 if (!bparams.output_changes || oldChange == change)
427 if (oldChange.type != Change::UNCHANGED) {
428 // close \lyxadded or \lyxdeleted
431 if (oldChange.type == Change::DELETED)
432 --runparams.inulemcmd;
436 chgTime += asctime(gmtime(&change.changetime));
437 // remove trailing '\n'
438 chgTime.erase(chgTime.end() - 1);
441 if (change.type == Change::DELETED) {
442 macro_beg = from_ascii("\\lyxdeleted{");
443 ++runparams.inulemcmd;
445 else if (change.type == Change::INSERTED)
446 macro_beg = from_ascii("\\lyxadded{");
448 docstring str = getLaTeXMarkup(macro_beg,
449 bparams.authors().get(change.author).name(),
452 // signature needed by \lyxsout to correctly strike out display math
453 if (change.type == Change::DELETED && runparams.inDisplayMath
454 && (!LaTeXFeatures::isAvailable("dvipost")
455 || (runparams.flavor != OutputParams::LATEX
456 && runparams.flavor != OutputParams::DVILUATEX))) {
457 if (os.afterParbreak())
458 str += from_ascii("\\\\\\noindent\n");
460 str += from_ascii("\\\\\\\\\n");
464 column += str.size();
470 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
471 Change const & old, Change const & change)
478 int const buffer_id = bparams.authors().get(change.author).bufferId();
480 switch (change.type) {
481 case Change::UNCHANGED:
482 os << "\n\\change_unchanged\n";
485 case Change::DELETED:
486 os << "\n\\change_deleted " << buffer_id
487 << " " << change.changetime << "\n";
490 case Change::INSERTED:
491 os << "\n\\change_inserted " << buffer_id
492 << " " << change.changetime << "\n";
498 void Changes::checkAuthors(AuthorList const & authorList)
500 ChangeTable::const_iterator it = table_.begin();
501 ChangeTable::const_iterator endit = table_.end();
502 for ( ; it != endit ; ++it)
503 if (it->change.type != Change::UNCHANGED)
504 authorList.get(it->change.author).setUsed(true);
508 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
509 bool output_active) const
514 shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
515 AuthorList const & author_list = buffer.params().authors();
516 DocIterator dit = cdit;
518 ChangeTable::const_iterator it = table_.begin();
519 ChangeTable::const_iterator const itend = table_.end();
520 for (; it != itend; ++it) {
522 switch (it->change.type) {
523 case Change::UNCHANGED:
525 case Change::DELETED:
526 // ✂ U+2702 BLACK SCISSORS
527 str.push_back(0x2702);
529 case Change::INSERTED:
530 // ✍ U+270D WRITING HAND
531 str.push_back(0x270d);
534 dit.pos() = it->range.start;
535 Paragraph const & par = dit.paragraph();
536 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
537 if (it->range.end > par.size())
538 // ¶ U+00B6 PILCROW SIGN
540 docstring const & author = author_list.get(it->change.author).name();
541 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
542 if (it == change_list->end()) {
543 change_list->push_back(TocItem(dit, 0, author, true));
544 change_list->push_back(TocItem(dit, 1, str, output_active));
547 for (++it; it != change_list->end(); ++it) {
548 if (it->depth() == 0 && it->str() != author)
551 change_list->insert(it, TocItem(dit, 1, str, output_active));
556 void Changes::updateBuffer(Buffer const & buf)
558 is_update_required_ = false;
559 if (!buf.areChangesPresent() && isChanged())
560 buf.setChangesPresent(true);
564 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
565 double const x2, FontInfo const & font) const
569 // Calculate 1/3 height of font
570 FontMetrics const & fm = theFontMetrics(font);
571 double const y_bar = deleted() ? y - fm.maxAscent() / 3
572 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
573 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
574 Painter::line_solid, pi.base.solidLineThickness());
578 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
579 double const x2, double const y2) const
594 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
595 color(), Painter::line_solid,
596 pi.base.solidLineThickness());
599 // FIXME: we cannot use antialias since we keep drawing on the same
600 // background with the current painting mechanism.
601 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
602 color(), Painter::line_solid_aliased,
603 pi.base.solidLineThickness());