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 "OutputParams.h"
23 #include "Paragraph.h"
24 #include "TocBackend.h"
26 #include "support/debug.h"
27 #include "support/gettext.h"
28 #include "support/lassert.h"
29 #include "support/lstrings.h"
31 #include "frontends/alert.h"
40 * Class Change has a changetime field that specifies the exact time at which
41 * a specific change was made. The change time is used as a guidance for the
42 * user while editing his document. Presently, it is not considered for LaTeX
44 * When merging two adjacent changes, the changetime is not considered,
45 * only the equality of the change type and author is checked (in method
46 * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
47 * the later change time is preserved.
50 bool Change::isSimilarTo(Change const & change) const
52 if (type != change.type)
55 if (type == Change::UNCHANGED)
58 return author == change.author;
62 Color Change::color() const
64 Color color = Color_none;
67 color = Color_changedtextauthor1;
70 color = Color_changedtextauthor2;
73 color = Color_changedtextauthor3;
76 color = Color_changedtextauthor4;
79 color = Color_changedtextauthor5;
84 color.mergeColor = Color_deletedtextmodifier;
90 bool operator==(Change const & l, Change const & r)
95 // two changes of type UNCHANGED are always equal
96 if (l.type == Change::UNCHANGED)
99 return l.author == r.author && l.changetime == r.changetime;
103 bool operator!=(Change const & l, Change const & r)
109 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
111 return r1.start == r2.start && r1.end == r2.end;
115 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
121 bool Changes::Range::intersects(Range const & r) const
123 return r.start < end && r.end > start; // end itself is not in the range!
127 void Changes::set(Change const & change, pos_type const pos)
129 set(change, pos, pos + 1);
133 void Changes::set(Change const & change, pos_type const start, pos_type const end)
135 if (change.type != Change::UNCHANGED) {
136 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
137 << ", author: " << change.author
138 << ", time: " << long(change.changetime)
139 << ") in range (" << start << ", " << end << ")");
142 Range const newRange(start, end);
144 ChangeTable::iterator it = table_.begin();
146 for (; it != table_.end(); ) {
147 // current change starts like or follows new change
148 if (it->range.start >= start) {
152 // new change intersects with existing change
153 if (it->range.end > start) {
154 pos_type oldEnd = it->range.end;
155 it->range.end = start;
157 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
158 << " resulting in range (" << it->range.start << ", "
159 << it->range.end << ")");
163 LYXERR(Debug::CHANGES, " inserting tail in range ("
164 << end << ", " << oldEnd << ")");
165 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
173 if (change.type != Change::UNCHANGED) {
174 LYXERR(Debug::CHANGES, " inserting change");
175 it = table_.insert(it, ChangeRange(change, Range(start, end)));
179 for (; it != table_.end(); ) {
180 // new change 'contains' existing change
181 if (newRange.contains(it->range)) {
182 LYXERR(Debug::CHANGES, " removing subrange ("
183 << it->range.start << ", " << it->range.end << ")");
184 it = table_.erase(it);
188 // new change precedes existing change
189 if (it->range.start >= end)
192 // new change intersects with existing change
193 it->range.start = end;
194 LYXERR(Debug::CHANGES, " cutting head of type "
195 << it->change.type << " resulting in range ("
196 << end << ", " << it->range.end << ")");
197 break; // no need for another iteration
204 void Changes::erase(pos_type const pos)
206 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
208 ChangeTable::iterator it = table_.begin();
209 ChangeTable::iterator end = table_.end();
211 for (; it != end; ++it) {
212 // range (pos,pos+x) becomes (pos,pos+x-1)
213 if (it->range.start > pos)
215 // range (pos-x,pos) stays (pos-x,pos)
216 if (it->range.end > pos)
224 void Changes::insert(Change const & change, lyx::pos_type pos)
226 if (change.type != Change::UNCHANGED) {
227 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
228 << " at position " << pos);
231 ChangeTable::iterator it = table_.begin();
232 ChangeTable::iterator end = table_.end();
234 for (; it != end; ++it) {
235 // range (pos,pos+x) becomes (pos+1,pos+x+1)
236 if (it->range.start >= pos)
239 // range (pos-x,pos) stays as it is
240 if (it->range.end > pos)
244 set(change, pos, pos + 1); // set will call merge
248 Change const & Changes::lookup(pos_type const pos) const
250 static Change const noChange = Change(Change::UNCHANGED);
252 ChangeTable::const_iterator it = table_.begin();
253 ChangeTable::const_iterator const end = table_.end();
255 for (; it != end; ++it) {
256 if (it->range.contains(pos))
264 bool Changes::isDeleted(pos_type start, pos_type end) const
266 ChangeTable::const_iterator it = table_.begin();
267 ChangeTable::const_iterator const itend = table_.end();
269 for (; it != itend; ++it) {
270 if (it->range.contains(Range(start, end))) {
271 LYXERR(Debug::CHANGES, "range ("
272 << start << ", " << end << ") fully contains ("
273 << it->range.start << ", " << it->range.end
274 << ") of type " << it->change.type);
275 return it->change.type == Change::DELETED;
282 bool Changes::isChanged(pos_type const start, pos_type const end) const
284 ChangeTable::const_iterator it = table_.begin();
285 ChangeTable::const_iterator const itend = table_.end();
287 for (; it != itend; ++it) {
288 if (it->range.intersects(Range(start, end))) {
289 LYXERR(Debug::CHANGES, "found intersection of range ("
290 << start << ", " << end << ") with ("
291 << it->range.start << ", " << it->range.end
292 << ") of type " << it->change.type);
300 void Changes::merge()
302 ChangeTable::iterator it = table_.begin();
304 while (it != table_.end()) {
305 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
306 << " and range (" << it->range.start << ", " << it->range.end
309 if (it->range.start == it->range.end) {
310 LYXERR(Debug::CHANGES, "removing empty range for pos "
319 if (it + 1 == table_.end())
322 if (it->change.isSimilarTo((it + 1)->change)
323 && it->range.end == (it + 1)->range.start) {
324 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
325 << it->range.end << ") and (" << (it + 1)->range.start << ", "
326 << (it + 1)->range.end << ")");
328 (it + 1)->range.start = it->range.start;
329 (it + 1)->change.changetime = max(it->change.changetime,
330 (it + 1)->change.changetime);
343 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
344 docstring const & chgTime,
345 OutputParams const & runparams)
350 static docstring warned_author = docstring();
351 docstring uncodable_author = warned_author;
352 odocstringstream ods;
355 // convert utf8 author name to something representable
356 // in the current encoding
357 docstring author_latexed;
358 for (size_t n = 0; n < author.size(); ++n) {
360 author_latexed += runparams.encoding->latexChar(author[n]);
361 } catch (EncodingException & /* e */) {
362 if (runparams.dryrun) {
363 ods << "<" << _("LyX Warning: ")
364 << _("uncodable character") << " '";
368 LYXERR0("Omitting uncodable character '"
369 << docstring(1, author[n])
370 << "' in change author name!");
371 uncodable_author = author;
375 ods << author_latexed << "}{" << chgTime << "}{";
377 // warn user (once) if we found uncodable glyphs.
378 if (uncodable_author != warned_author) {
379 frontend::Alert::warning(_("Uncodable character in author name"),
380 support::bformat(_("The author name '%1$s',\n"
381 "used for change tracking, contains glyphs that cannot be\n"
382 "represented in the current encoding. The respective glyphs\n"
383 "will be omitted in the exported LaTeX file.\n\n"
384 "Choose an appropriate document encoding (such as utf8)\n"
385 "or change the spelling of the author name."), uncodable_author));
386 warned_author = uncodable_author;
394 int Changes::latexMarkChange(odocstream & os, BufferParams const & bparams,
395 Change const & oldChange, Change const & change,
396 OutputParams const & runparams)
398 if (!bparams.outputChanges || oldChange == change)
403 if (oldChange.type != Change::UNCHANGED) {
404 // close \lyxadded or \lyxdeleted
410 chgTime += ctime(&change.changetime);
411 // remove trailing '\n'
412 chgTime.erase(chgTime.end() - 1);
415 if (change.type == Change::DELETED)
416 macro_beg = from_ascii("\\lyxdeleted{");
417 else if (change.type == Change::INSERTED)
418 macro_beg = from_ascii("\\lyxadded{");
420 docstring str = getLaTeXMarkup(macro_beg,
421 bparams.authors().get(change.author).name(),
425 column += str.size();
431 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
432 Change const & old, Change const & change)
439 int const buffer_id = bparams.authors().get(change.author).bufferId();
441 switch (change.type) {
442 case Change::UNCHANGED:
443 os << "\n\\change_unchanged\n";
446 case Change::DELETED:
447 os << "\n\\change_deleted " << buffer_id
448 << " " << change.changetime << "\n";
451 case Change::INSERTED:
452 os << "\n\\change_inserted " << buffer_id
453 << " " << change.changetime << "\n";
459 void Changes::checkAuthors(AuthorList const & authorList)
461 ChangeTable::const_iterator it = table_.begin();
462 ChangeTable::const_iterator endit = table_.end();
463 for ( ; it != endit ; ++it)
464 if (it->change.type != Change::UNCHANGED)
465 authorList.get(it->change.author).setUsed(true);
469 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer) const
474 Toc & change_list = buffer.tocBackend().toc("change");
475 AuthorList const & author_list = buffer.params().authors();
476 DocIterator dit = cdit;
478 ChangeTable::const_iterator it = table_.begin();
479 ChangeTable::const_iterator const itend = table_.end();
480 for (; it != itend; ++it) {
482 switch (it->change.type) {
483 case Change::UNCHANGED:
485 case Change::DELETED:
486 // 0x2702 is a scissors symbol in the Dingbats unicode group.
487 str.push_back(0x2702);
489 case Change::INSERTED:
490 // 0x270d is the hand writting symbol in the Dingbats unicode group.
491 str.push_back(0x270d);
494 dit.pos() = it->range.start;
495 Paragraph const & par = dit.paragraph();
496 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
497 if (it->range.end > par.size())
498 // the end of paragraph symbol from the Punctuation group
499 str.push_back(0x204B);
500 docstring const & author = author_list.get(it->change.author).name();
501 Toc::iterator it = change_list.item(0, author);
502 if (it == change_list.end()) {
503 change_list.push_back(TocItem(dit, 0, author));
504 change_list.push_back(TocItem(dit, 1, str,
505 support::wrapParas(str, 4)));
508 for (++it; it != change_list.end(); ++it) {
509 if (it->depth() == 0 && it->str() != author)
512 change_list.insert(it, TocItem(dit, 1, str,
513 support::wrapParas(str, 4)));