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 << ")");
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, docstring const & author,
342 docstring const & chgTime,
343 OutputParams const & runparams)
348 docstring uncodable_author;
349 odocstringstream ods;
352 // convert utf8 author name to something representable
353 // in the current encoding
354 pair<docstring, docstring> author_latexed =
355 runparams.encoding->latexString(author, runparams.dryrun);
356 if (!author_latexed.second.empty()) {
357 LYXERR0("Omitting uncodable characters '"
358 << author_latexed.second
359 << "' in change author name!");
360 uncodable_author = author;
362 ods << author_latexed.first << "}{" << chgTime << "}{";
364 // warn user (once) if we found uncodable glyphs.
365 if (!uncodable_author.empty()) {
366 static std::set<docstring> warned_authors;
367 static Mutex warned_mutex;
368 Mutex::Locker locker(&warned_mutex);
369 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
370 frontend::Alert::warning(_("Uncodable character in author name"),
371 support::bformat(_("The author name '%1$s',\n"
372 "used for change tracking, contains the following glyphs that\n"
373 "cannot be represented in the current encoding: %2$s.\n"
374 "These glyphs will be omitted in the exported LaTeX file.\n\n"
375 "Choose an appropriate document encoding (such as utf8)\n"
376 "or change the spelling of the author name."),
377 uncodable_author, author_latexed.second));
378 warned_authors.insert(uncodable_author);
388 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
389 Change const & oldChange, Change const & change,
390 OutputParams const & runparams)
392 if (!bparams.output_changes || oldChange == change)
397 if (oldChange.type != Change::UNCHANGED) {
398 // close \lyxadded or \lyxdeleted
401 if (oldChange.type == Change::DELETED && !runparams.wasDisplayMath)
402 --runparams.inulemcmd;
406 chgTime += asctime(gmtime(&change.changetime));
407 // remove trailing '\n'
408 chgTime.erase(chgTime.end() - 1);
411 if (change.type == Change::DELETED) {
412 macro_beg = from_ascii("\\lyxdeleted{");
413 if (!runparams.inDisplayMath)
414 ++runparams.inulemcmd;
416 else if (change.type == Change::INSERTED)
417 macro_beg = from_ascii("\\lyxadded{");
419 docstring str = getLaTeXMarkup(macro_beg,
420 bparams.authors().get(change.author).name(),
423 // signature needed by \lyxsout to correctly strike out display math
424 if (change.type == Change::DELETED && runparams.inDisplayMath
425 && (!LaTeXFeatures::isAvailable("dvipost")
426 || (runparams.flavor != OutputParams::LATEX
427 && runparams.flavor != OutputParams::DVILUATEX))) {
428 if (os.afterParbreak())
429 str += from_ascii("\\\\\\noindent\n");
431 str += from_ascii("\\\\\\\\\n");
435 column += str.size();
441 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
442 Change const & old, Change const & change)
449 int const buffer_id = bparams.authors().get(change.author).bufferId();
451 switch (change.type) {
452 case Change::UNCHANGED:
453 os << "\n\\change_unchanged\n";
456 case Change::DELETED:
457 os << "\n\\change_deleted " << buffer_id
458 << " " << change.changetime << "\n";
461 case Change::INSERTED:
462 os << "\n\\change_inserted " << buffer_id
463 << " " << change.changetime << "\n";
469 void Changes::checkAuthors(AuthorList const & authorList)
471 for (ChangeRange const & cr : table_)
472 if (cr.change.type != Change::UNCHANGED)
473 authorList.get(cr.change.author).setUsed(true);
477 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
478 bool output_active, TocBackend & backend) const
483 shared_ptr<Toc> change_list = backend.toc("change");
484 AuthorList const & author_list = buffer.params().authors();
485 DocIterator dit = cdit;
487 for (ChangeRange const & cr : table_) {
489 switch (cr.change.type) {
490 case Change::UNCHANGED:
492 case Change::DELETED:
493 // ✂ U+2702 BLACK SCISSORS
494 str.push_back(0x2702);
496 case Change::INSERTED:
497 // ✍ U+270D WRITING HAND
498 str.push_back(0x270d);
501 dit.pos() = cr.range.start;
502 Paragraph const & par = dit.paragraph();
503 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
504 if (cr.range.end > par.size())
505 // ¶ U+00B6 PILCROW SIGN
507 docstring const & author = author_list.get(cr.change.author).name();
508 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
509 if (it == change_list->end()) {
510 change_list->push_back(TocItem(dit, 0, author, true));
511 change_list->push_back(TocItem(dit, 1, str, output_active));
514 for (++it; it != change_list->end(); ++it) {
515 if (it->depth() == 0 && it->str() != author)
518 change_list->insert(it, TocItem(dit, 1, str, output_active));
523 void Changes::updateBuffer(Buffer const & buf)
525 bool const changed = isChanged();
526 buf.setChangesPresent(buf.areChangesPresent() || changed);
527 previously_changed_ = changed;
531 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
532 double const x2, FontInfo const & font) const
536 // Calculate 1/3 height of font
537 FontMetrics const & fm = theFontMetrics(font);
538 double const y_bar = deleted() ? y - fm.maxAscent() / 3
539 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
540 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
541 Painter::line_solid, pi.base.solidLineThickness());
545 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
546 double const x2, double const y2) const
561 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
562 color(), Painter::line_solid,
563 pi.base.solidLineThickness());
566 // FIXME: we cannot use antialias since we keep drawing on the same
567 // background with the current painting mechanism.
568 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
569 color(), Painter::line_solid_aliased,
570 pi.base.solidLineThickness());