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 bool const dvipost = LaTeXFeatures::isAvailable("dvipost") &&
398 (runparams.flavor == OutputParams::LATEX
399 || runparams.flavor == OutputParams::DVILUATEX);
401 if (oldChange.type != Change::UNCHANGED) {
402 // close \lyxadded or \lyxdeleted
405 if (oldChange.type == Change::DELETED
406 && !runparams.wasDisplayMath && !dvipost)
407 --runparams.inulemcmd;
411 chgTime += asctime(gmtime(&change.changetime));
412 // remove trailing '\n'
413 chgTime.erase(chgTime.end() - 1);
416 if (change.type == Change::DELETED) {
417 macro_beg = from_ascii("\\lyxdeleted{");
418 if (!runparams.inDisplayMath && !dvipost)
419 ++runparams.inulemcmd;
421 else if (change.type == Change::INSERTED)
422 macro_beg = from_ascii("\\lyxadded{");
424 docstring str = getLaTeXMarkup(macro_beg,
425 bparams.authors().get(change.author).name(),
428 // signature needed by \lyxsout to correctly strike out display math
429 if (change.type == Change::DELETED && runparams.inDisplayMath
432 str += from_ascii("\\\\\\noindent\n");
434 str += from_ascii("\\\\\\\\\n");
438 column += str.size();
444 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
445 Change const & old, Change const & change)
452 int const buffer_id = bparams.authors().get(change.author).bufferId();
454 switch (change.type) {
455 case Change::UNCHANGED:
456 os << "\n\\change_unchanged\n";
459 case Change::DELETED:
460 os << "\n\\change_deleted " << buffer_id
461 << " " << change.changetime << "\n";
464 case Change::INSERTED:
465 os << "\n\\change_inserted " << buffer_id
466 << " " << change.changetime << "\n";
472 void Changes::checkAuthors(AuthorList const & authorList)
474 for (ChangeRange const & cr : table_)
475 if (cr.change.type != Change::UNCHANGED)
476 authorList.get(cr.change.author).setUsed(true);
480 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
481 bool output_active, TocBackend & backend) const
486 shared_ptr<Toc> change_list = backend.toc("change");
487 AuthorList const & author_list = buffer.params().authors();
488 DocIterator dit = cdit;
490 for (ChangeRange const & cr : table_) {
492 switch (cr.change.type) {
493 case Change::UNCHANGED:
495 case Change::DELETED:
496 // ✂ U+2702 BLACK SCISSORS
497 str.push_back(0x2702);
499 case Change::INSERTED:
500 // ✍ U+270D WRITING HAND
501 str.push_back(0x270d);
504 dit.pos() = cr.range.start;
505 Paragraph const & par = dit.paragraph();
506 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
507 if (cr.range.end > par.size())
508 // ¶ U+00B6 PILCROW SIGN
510 docstring const & author = author_list.get(cr.change.author).name();
511 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
512 if (it == change_list->end()) {
513 change_list->push_back(TocItem(dit, 0, author, true));
514 change_list->push_back(TocItem(dit, 1, str, output_active));
517 for (++it; it != change_list->end(); ++it) {
518 if (it->depth() == 0 && it->str() != author)
521 change_list->insert(it, TocItem(dit, 1, str, output_active));
526 void Changes::updateBuffer(Buffer const & buf)
528 bool const changed = isChanged();
529 buf.setChangesPresent(buf.areChangesPresent() || changed);
530 previously_changed_ = changed;
534 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
535 double const x2, FontInfo const & font) const
539 // Calculate 1/3 height of font
540 FontMetrics const & fm = theFontMetrics(font);
541 double const y_bar = deleted() ? y - fm.maxAscent() / 3
542 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
543 pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
544 Painter::line_solid, pi.base.solidLineThickness());
548 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
549 double const x2, double const y2) const
564 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
565 color(), Painter::line_solid,
566 pi.base.solidLineThickness());
569 // FIXME: we cannot use antialias since we keep drawing on the same
570 // background with the current painting mechanism.
571 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
572 color(), Painter::line_solid_aliased,
573 pi.base.solidLineThickness());