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(),
453 column += str.size();
459 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
460 Change const & old, Change const & change)
467 int const buffer_id = bparams.authors().get(change.author).bufferId();
469 switch (change.type) {
470 case Change::UNCHANGED:
471 os << "\n\\change_unchanged\n";
474 case Change::DELETED:
475 os << "\n\\change_deleted " << buffer_id
476 << " " << change.changetime << "\n";
479 case Change::INSERTED:
480 os << "\n\\change_inserted " << buffer_id
481 << " " << change.changetime << "\n";
487 void Changes::checkAuthors(AuthorList const & authorList)
489 ChangeTable::const_iterator it = table_.begin();
490 ChangeTable::const_iterator endit = table_.end();
491 for ( ; it != endit ; ++it)
492 if (it->change.type != Change::UNCHANGED)
493 authorList.get(it->change.author).setUsed(true);
497 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
498 bool output_active) const
503 shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
504 AuthorList const & author_list = buffer.params().authors();
505 DocIterator dit = cdit;
507 ChangeTable::const_iterator it = table_.begin();
508 ChangeTable::const_iterator const itend = table_.end();
509 for (; it != itend; ++it) {
511 switch (it->change.type) {
512 case Change::UNCHANGED:
514 case Change::DELETED:
515 // ✂ U+2702 BLACK SCISSORS
516 str.push_back(0x2702);
518 case Change::INSERTED:
519 // ✍ U+270D WRITING HAND
520 str.push_back(0x270d);
523 dit.pos() = it->range.start;
524 Paragraph const & par = dit.paragraph();
525 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
526 if (it->range.end > par.size())
527 // ¶ U+00B6 PILCROW SIGN
529 docstring const & author = author_list.get(it->change.author).name();
530 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
531 if (it == change_list->end()) {
532 change_list->push_back(TocItem(dit, 0, author, true));
533 change_list->push_back(TocItem(dit, 1, str, output_active));
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));
545 void Changes::updateBuffer(Buffer const & buf)
547 is_update_required_ = false;
548 if (!buf.areChangesPresent() && isChanged())
549 buf.setChangesPresent(true);
553 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
554 double const x2, FontInfo const & font) const
558 // Calculate 1/3 height of font
559 FontMetrics const & fm = theFontMetrics(font);
560 double const y_bar = deleted() ? y - fm.maxAscent() / 3
561 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
562 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
563 Painter::line_solid, pi.base.solidLineThickness());
567 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
568 double const x2, double const y2) const
583 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
584 color(), Painter::line_solid,
585 pi.base.solidLineThickness());
588 // FIXME: we cannot use antialias since we keep drawing on the same
589 // background with the current painting mechanism.
590 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
591 color(), Painter::line_solid_aliased,
592 pi.base.solidLineThickness());