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 "TocBackend.h"
27 #include "support/debug.h"
28 #include "support/gettext.h"
29 #include "support/lassert.h"
30 #include "support/lstrings.h"
31 #include "support/mutex.h"
33 #include "frontends/alert.h"
34 #include "frontends/FontMetrics.h"
35 #include "frontends/Painter.h"
43 using frontend::Painter;
44 using frontend::FontMetrics;
47 * Class Change has a changetime field that specifies the exact time at which
48 * a specific change was made. The change time is used as a guidance for the
49 * user while editing his document. Presently, it is not considered for LaTeX
51 * When merging two adjacent changes, the changetime is not considered,
52 * only the equality of the change type and author is checked (in method
53 * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
54 * the later change time is preserved.
57 bool Change::isSimilarTo(Change const & change) const
59 if (type != change.type)
62 if (type == Change::UNCHANGED)
65 return author == change.author;
69 Color Change::color() const
71 Color color = Color_none;
74 color = Color_changedtextauthor1;
77 color = Color_changedtextauthor2;
80 color = Color_changedtextauthor3;
83 color = Color_changedtextauthor4;
86 color = Color_changedtextauthor5;
91 color.mergeColor = Color_deletedtextmodifier;
97 bool operator==(Change const & l, Change const & r)
102 // two changes of type UNCHANGED are always equal
103 if (l.type == Change::UNCHANGED)
106 return l.author == r.author && l.changetime == r.changetime;
110 bool operator!=(Change const & l, Change const & r)
116 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
118 return r1.start == r2.start && r1.end == r2.end;
122 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
128 bool Changes::Range::intersects(Range const & r) const
130 return r.start < end && r.end > start; // end itself is not in the range!
134 void Changes::set(Change const & change, pos_type const pos)
136 set(change, pos, pos + 1);
140 void Changes::set(Change const & change, pos_type const start, pos_type const end)
142 if (change.type != Change::UNCHANGED) {
143 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
144 << ", author: " << change.author
145 << ", time: " << long(change.changetime)
146 << ") in range (" << start << ", " << end << ")");
148 is_update_required_ = true;
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 ChangeTable::iterator it = table_.begin();
218 ChangeTable::iterator end = table_.end();
220 for (; it != end; ++it) {
221 // range (pos,pos+x) becomes (pos,pos+x-1)
222 if (it->range.start > pos)
224 // range (pos-x,pos) stays (pos-x,pos)
225 if (it->range.end > pos)
233 void Changes::insert(Change const & change, lyx::pos_type pos)
235 if (change.type != Change::UNCHANGED) {
236 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
237 << " at position " << pos);
240 ChangeTable::iterator it = table_.begin();
241 ChangeTable::iterator end = table_.end();
243 for (; it != end; ++it) {
244 // range (pos,pos+x) becomes (pos+1,pos+x+1)
245 if (it->range.start >= pos)
248 // range (pos-x,pos) stays as it is
249 if (it->range.end > pos)
253 set(change, pos, pos + 1); // set will call merge
257 Change const & Changes::lookup(pos_type const pos) const
259 static Change const noChange = Change(Change::UNCHANGED);
261 ChangeTable::const_iterator it = table_.begin();
262 ChangeTable::const_iterator const end = table_.end();
264 for (; it != end; ++it) {
265 if (it->range.contains(pos))
273 bool Changes::isDeleted(pos_type start, pos_type end) const
275 ChangeTable::const_iterator it = table_.begin();
276 ChangeTable::const_iterator const itend = table_.end();
278 for (; it != itend; ++it) {
279 if (it->range.contains(Range(start, end))) {
280 LYXERR(Debug::CHANGES, "range ("
281 << start << ", " << end << ") fully contains ("
282 << it->range.start << ", " << it->range.end
283 << ") of type " << it->change.type);
284 return it->change.type == Change::DELETED;
291 bool Changes::isChanged(pos_type const start, pos_type const end) const
293 ChangeTable::const_iterator it = table_.begin();
294 ChangeTable::const_iterator const itend = table_.end();
296 for (; it != itend; ++it) {
297 if (it->range.intersects(Range(start, end))) {
298 LYXERR(Debug::CHANGES, "found intersection of range ("
299 << start << ", " << end << ") with ("
300 << it->range.start << ", " << it->range.end
301 << ") of type " << it->change.type);
309 bool Changes::isChanged() const
311 ChangeTable::const_iterator it = table_.begin();
312 ChangeTable::const_iterator const itend = table_.end();
313 for (; it != itend; ++it) {
314 if (it->change.changed())
321 void Changes::merge()
324 ChangeTable::iterator it = table_.begin();
326 while (it != table_.end()) {
327 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
328 << " and range (" << it->range.start << ", " << it->range.end
331 if (it->range.start == it->range.end) {
332 LYXERR(Debug::CHANGES, "removing empty range for pos "
342 if (it + 1 == table_.end())
345 if (it->change.isSimilarTo((it + 1)->change)
346 && it->range.end == (it + 1)->range.start) {
347 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
348 << it->range.end << ") and (" << (it + 1)->range.start << ", "
349 << (it + 1)->range.end << ")");
351 (it + 1)->range.start = it->range.start;
352 (it + 1)->change.changetime = max(it->change.changetime,
353 (it + 1)->change.changetime);
363 if (merged && !isChanged())
364 is_update_required_ = true;
370 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
371 docstring const & chgTime,
372 OutputParams const & runparams)
377 docstring uncodable_author;
378 odocstringstream ods;
381 // convert utf8 author name to something representable
382 // in the current encoding
383 pair<docstring, docstring> author_latexed =
384 runparams.encoding->latexString(author, runparams.dryrun);
385 if (!author_latexed.second.empty()) {
386 LYXERR0("Omitting uncodable characters '"
387 << author_latexed.second
388 << "' in change author name!");
389 uncodable_author = author;
391 ods << author_latexed.first << "}{" << chgTime << "}{";
393 // warn user (once) if we found uncodable glyphs.
394 if (!uncodable_author.empty()) {
395 static std::set<docstring> warned_authors;
396 static Mutex warned_mutex;
397 Mutex::Locker locker(&warned_mutex);
398 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
399 frontend::Alert::warning(_("Uncodable character in author name"),
400 support::bformat(_("The author name '%1$s',\n"
401 "used for change tracking, contains the following glyphs that\n"
402 "cannot be represented in the current encoding: %2$s.\n"
403 "These glyphs will be omitted in the exported LaTeX file.\n\n"
404 "Choose an appropriate document encoding (such as utf8)\n"
405 "or change the spelling of the author name."),
406 uncodable_author, author_latexed.second));
407 warned_authors.insert(uncodable_author);
417 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
418 Change const & oldChange, Change const & change,
419 OutputParams const & runparams)
421 if (!bparams.output_changes || oldChange == change)
426 if (oldChange.type != Change::UNCHANGED) {
427 // close \lyxadded or \lyxdeleted
430 if (oldChange.type == Change::DELETED)
431 --runparams.inulemcmd;
435 chgTime += asctime(gmtime(&change.changetime));
436 // remove trailing '\n'
437 chgTime.erase(chgTime.end() - 1);
440 if (change.type == Change::DELETED) {
441 macro_beg = from_ascii("\\lyxdeleted{");
442 ++runparams.inulemcmd;
444 else if (change.type == Change::INSERTED)
445 macro_beg = from_ascii("\\lyxadded{");
447 docstring str = getLaTeXMarkup(macro_beg,
448 bparams.authors().get(change.author).name(),
452 column += str.size();
458 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
459 Change const & old, Change const & change)
466 int const buffer_id = bparams.authors().get(change.author).bufferId();
468 switch (change.type) {
469 case Change::UNCHANGED:
470 os << "\n\\change_unchanged\n";
473 case Change::DELETED:
474 os << "\n\\change_deleted " << buffer_id
475 << " " << change.changetime << "\n";
478 case Change::INSERTED:
479 os << "\n\\change_inserted " << buffer_id
480 << " " << change.changetime << "\n";
486 void Changes::checkAuthors(AuthorList const & authorList)
488 ChangeTable::const_iterator it = table_.begin();
489 ChangeTable::const_iterator endit = table_.end();
490 for ( ; it != endit ; ++it)
491 if (it->change.type != Change::UNCHANGED)
492 authorList.get(it->change.author).setUsed(true);
496 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
497 bool output_active) const
502 shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
503 AuthorList const & author_list = buffer.params().authors();
504 DocIterator dit = cdit;
506 ChangeTable::const_iterator it = table_.begin();
507 ChangeTable::const_iterator const itend = table_.end();
508 for (; it != itend; ++it) {
510 switch (it->change.type) {
511 case Change::UNCHANGED:
513 case Change::DELETED:
514 // ✂ U+2702 BLACK SCISSORS
515 str.push_back(0x2702);
517 case Change::INSERTED:
518 // ✍ U+270D WRITING HAND
519 str.push_back(0x270d);
522 dit.pos() = it->range.start;
523 Paragraph const & par = dit.paragraph();
524 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
525 if (it->range.end > par.size())
526 // ¶ U+00B6 PILCROW SIGN
528 docstring const & author = author_list.get(it->change.author).name();
529 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
530 if (it == change_list->end()) {
531 change_list->push_back(TocItem(dit, 0, author, true));
532 change_list->push_back(TocItem(dit, 1, str, output_active,
533 support::wrapParas(str, 4)));
536 for (++it; it != change_list->end(); ++it) {
537 if (it->depth() == 0 && it->str() != author)
540 change_list->insert(it, TocItem(dit, 1, str, output_active,
541 support::wrapParas(str, 4)));
546 void Changes::updateBuffer(Buffer const & buf)
548 is_update_required_ = false;
549 if (!buf.areChangesPresent() && isChanged())
550 buf.setChangesPresent(true);
554 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
555 double const x2, FontInfo const & font) const
559 // Calculate 1/3 height of font
560 FontMetrics const & fm = theFontMetrics(font);
561 int const y_bar = deleted() ? y - fm.maxAscent() / 3
562 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
563 pi.pain.line(int(x1), y_bar, int(x2), y_bar, color(),
564 Painter::line_solid, pi.base.solidLineThickness());
568 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
569 double const x2, double const y2) const
591 pi.pain.line(x1, y2, x2, y, color(), Painter::line_solid,
592 pi.base.solidLineThickness());