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"
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_author);
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 bool const dvipost = LaTeXFeatures::isAvailable("dvipost") &&
433 (runparams.flavor == OutputParams::LATEX
434 || runparams.flavor == OutputParams::DVILUATEX);
436 if (oldChange.type != Change::UNCHANGED) {
437 // close \lyxadded or \lyxdeleted
440 if (oldChange.type == Change::DELETED
441 && !runparams.wasDisplayMath && !dvipost)
442 --runparams.inulemcmd;
446 chgTime += asctime(gmtime(&change.changetime));
447 // remove trailing '\n'
448 chgTime.erase(chgTime.end() - 1);
451 if (change.type == Change::DELETED) {
452 macro_beg = from_ascii("\\lyxdeleted");
453 if (!runparams.inDisplayMath && !dvipost)
454 ++runparams.inulemcmd;
456 else if (change.type == Change::INSERTED)
457 macro_beg = from_ascii("\\lyxadded");
459 docstring str = getLaTeXMarkup(macro_beg,
460 bparams.authors().get(change.author),
463 // signature needed by \lyxsout to correctly strike out display math
464 if (change.type == Change::DELETED && runparams.inDisplayMath
467 str += from_ascii("\\\\\\noindent\n");
469 str += from_ascii("\\\\\\\\\n");
473 column += str.size();
479 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
480 Change const & old, Change const & change)
487 int const buffer_id = bparams.authors().get(change.author).bufferId();
489 switch (change.type) {
490 case Change::UNCHANGED:
491 os << "\n\\change_unchanged\n";
494 case Change::DELETED:
495 os << "\n\\change_deleted " << buffer_id
496 << " " << change.changetime << "\n";
499 case Change::INSERTED:
500 os << "\n\\change_inserted " << buffer_id
501 << " " << change.changetime << "\n";
507 void Changes::checkAuthors(AuthorList const & authorList)
509 for (ChangeRange const & cr : table_)
510 if (cr.change.type != Change::UNCHANGED)
511 authorList.get(cr.change.author).setUsed(true);
515 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
516 bool output_active, TocBackend & backend) const
521 shared_ptr<Toc> change_list = backend.toc("change");
522 AuthorList const & author_list = buffer.params().authors();
523 DocIterator dit = cdit;
525 for (ChangeRange const & cr : table_) {
527 switch (cr.change.type) {
528 case Change::UNCHANGED:
530 case Change::DELETED:
531 // ✂ U+2702 BLACK SCISSORS
532 str.push_back(0x2702);
534 case Change::INSERTED:
535 // ✍ U+270D WRITING HAND
536 str.push_back(0x270d);
539 dit.pos() = cr.range.start;
540 Paragraph const & par = dit.paragraph();
541 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
542 if (cr.range.end > par.size())
543 // ¶ U+00B6 PILCROW SIGN
545 docstring const & author = author_list.get(cr.change.author).name();
546 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
547 if (it == change_list->end()) {
548 change_list->push_back(TocItem(dit, 0, author, true));
549 change_list->push_back(TocItem(dit, 1, str, output_active));
552 for (++it; it != change_list->end(); ++it) {
553 if (it->depth() == 0 && it->str() != author)
556 change_list->insert(it, TocItem(dit, 1, str, output_active));
561 void Changes::updateBuffer(Buffer const & buf)
563 bool const changed = isChanged();
564 buf.setChangesPresent(buf.areChangesPresent() || changed);
565 previously_changed_ = changed;
569 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
570 double const x2, FontInfo const & font) const
572 if (!changed() || (!lyxrc.ct_additions_underlined && inserted()))
574 // Calculate 1/3 height of font
575 FontMetrics const & fm = theFontMetrics(font);
576 double const y_bar = deleted() ? y - fm.maxAscent() / 3
577 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
578 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
579 Painter::line_solid, pi.base.solidLineThickness());
583 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
584 double const x2, double const y2) const
599 if (!lyxrc.ct_additions_underlined)
601 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
602 color(), Painter::line_solid,
603 pi.base.solidLineThickness());
607 // FIXME: we cannot use antialias since we keep drawing on the same
608 // background with the current painting mechanism.
609 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
610 color(), Painter::line_solid_aliased,
611 pi.base.solidLineThickness());