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;
433 runparams.inDisplayMath = false;
438 chgTime += asctime(gmtime(&change.changetime));
439 // remove trailing '\n'
440 chgTime.erase(chgTime.end() - 1);
443 if (change.type == Change::DELETED) {
444 macro_beg = from_ascii("\\lyxdeleted{");
445 ++runparams.inulemcmd;
447 else if (change.type == Change::INSERTED)
448 macro_beg = from_ascii("\\lyxadded{");
450 docstring str = getLaTeXMarkup(macro_beg,
451 bparams.authors().get(change.author).name(),
454 // signature needed by \lyxsout to correctly strike out display math
455 if (change.type == Change::DELETED && runparams.inDisplayMath
456 && (!LaTeXFeatures::isAvailable("dvipost")
457 || (runparams.flavor != OutputParams::LATEX
458 && runparams.flavor != OutputParams::DVILUATEX))) {
459 if (os.afterParbreak())
460 str += from_ascii("\\\\\\noindent\n");
462 str += from_ascii("\\\\\\mbox{}\\\\\n");
466 column += str.size();
472 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
473 Change const & old, Change const & change)
480 int const buffer_id = bparams.authors().get(change.author).bufferId();
482 switch (change.type) {
483 case Change::UNCHANGED:
484 os << "\n\\change_unchanged\n";
487 case Change::DELETED:
488 os << "\n\\change_deleted " << buffer_id
489 << " " << change.changetime << "\n";
492 case Change::INSERTED:
493 os << "\n\\change_inserted " << buffer_id
494 << " " << change.changetime << "\n";
500 void Changes::checkAuthors(AuthorList const & authorList)
502 ChangeTable::const_iterator it = table_.begin();
503 ChangeTable::const_iterator endit = table_.end();
504 for ( ; it != endit ; ++it)
505 if (it->change.type != Change::UNCHANGED)
506 authorList.get(it->change.author).setUsed(true);
510 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
511 bool output_active) const
516 shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
517 AuthorList const & author_list = buffer.params().authors();
518 DocIterator dit = cdit;
520 ChangeTable::const_iterator it = table_.begin();
521 ChangeTable::const_iterator const itend = table_.end();
522 for (; it != itend; ++it) {
524 switch (it->change.type) {
525 case Change::UNCHANGED:
527 case Change::DELETED:
528 // ✂ U+2702 BLACK SCISSORS
529 str.push_back(0x2702);
531 case Change::INSERTED:
532 // ✍ U+270D WRITING HAND
533 str.push_back(0x270d);
536 dit.pos() = it->range.start;
537 Paragraph const & par = dit.paragraph();
538 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
539 if (it->range.end > par.size())
540 // ¶ U+00B6 PILCROW SIGN
542 docstring const & author = author_list.get(it->change.author).name();
543 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
544 if (it == change_list->end()) {
545 change_list->push_back(TocItem(dit, 0, author, true));
546 change_list->push_back(TocItem(dit, 1, str, output_active));
549 for (++it; it != change_list->end(); ++it) {
550 if (it->depth() == 0 && it->str() != author)
553 change_list->insert(it, TocItem(dit, 1, str, output_active));
558 void Changes::updateBuffer(Buffer const & buf)
560 is_update_required_ = false;
561 if (!buf.areChangesPresent() && isChanged())
562 buf.setChangesPresent(true);
566 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
567 double const x2, FontInfo const & font) const
571 // Calculate 1/3 height of font
572 FontMetrics const & fm = theFontMetrics(font);
573 double const y_bar = deleted() ? y - fm.maxAscent() / 3
574 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
575 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
576 Painter::line_solid, pi.base.solidLineThickness());
580 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
581 double const x2, double const y2) const
596 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
597 color(), Painter::line_solid,
598 pi.base.solidLineThickness());
601 // FIXME: we cannot use antialias since we keep drawing on the same
602 // background with the current painting mechanism.
603 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
604 color(), Painter::line_solid_aliased,
605 pi.base.solidLineThickness());