]> git.lyx.org Git - lyx.git/blob - src/Changes.cpp
Automatically show the review toolbar if the document has tracked changes
[lyx.git] / src / Changes.cpp
1 /**
2  * \file Changes.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author John Levon
7  * \author Michael Gerz
8  *
9  * Full author contact details are available in file CREDITS.
10  *
11  * Record changes in a paragraph.
12  */
13
14 #include <config.h>
15
16 #include "Changes.h"
17 #include "Author.h"
18 #include "Buffer.h"
19 #include "BufferParams.h"
20 #include "Encoding.h"
21 #include "LaTeXFeatures.h"
22 #include "OutputParams.h"
23 #include "Paragraph.h"
24 #include "TocBackend.h"
25
26 #include "support/debug.h"
27 #include "support/gettext.h"
28 #include "support/lassert.h"
29 #include "support/lstrings.h"
30 #include "support/mutex.h"
31
32 #include "frontends/alert.h"
33
34 #include <ostream>
35
36 using namespace std;
37
38 namespace lyx {
39
40 /*
41  * Class Change has a changetime field that specifies the exact time at which
42  * a specific change was made. The change time is used as a guidance for the
43  * user while editing his document. Presently, it is not considered for LaTeX
44  * export.
45  * When merging two adjacent changes, the changetime is not considered,
46  * only the equality of the change type and author is checked (in method
47  * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
48  * the later change time is preserved.
49  */
50
51 bool Change::isSimilarTo(Change const & change) const
52 {
53         if (type != change.type)
54                 return false;
55
56         if (type == Change::UNCHANGED)
57                 return true;
58
59         return author == change.author;
60 }
61
62
63 Color Change::color() const
64 {
65         Color color = Color_none;
66         switch (author % 5) {
67                 case 0:
68                         color = Color_changedtextauthor1;
69                         break;
70                 case 1:
71                         color = Color_changedtextauthor2;
72                         break;
73                 case 2:
74                         color = Color_changedtextauthor3;
75                         break;
76                 case 3:
77                         color = Color_changedtextauthor4;
78                         break;
79                 case 4:
80                         color = Color_changedtextauthor5;
81                         break;
82         }
83
84         if (deleted())
85                 color.mergeColor = Color_deletedtextmodifier;
86
87         return color;
88 }
89
90
91 bool operator==(Change const & l, Change const & r)
92 {
93         if (l.type != r.type)
94                 return false;
95
96         // two changes of type UNCHANGED are always equal
97         if (l.type == Change::UNCHANGED)
98                 return true;
99
100         return l.author == r.author && l.changetime == r.changetime;
101 }
102
103
104 bool operator!=(Change const & l, Change const & r)
105 {
106         return !(l == r);
107 }
108
109
110 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
111 {
112         return r1.start == r2.start && r1.end == r2.end;
113 }
114
115
116 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
117 {
118         return !(r1 == r2);
119 }
120
121
122 bool Changes::Range::intersects(Range const & r) const
123 {
124         return r.start < end && r.end > start; // end itself is not in the range!
125 }
126
127
128 void Changes::set(Change const & change, pos_type const pos)
129 {
130         set(change, pos, pos + 1);
131 }
132
133
134 void Changes::set(Change const & change, pos_type const start, pos_type const end)
135 {
136         if (change.type != Change::UNCHANGED) {
137                 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
138                         << ", author: " << change.author 
139                         << ", time: " << long(change.changetime)
140                         << ") in range (" << start << ", " << end << ")");
141         }
142
143         Range const newRange(start, end);
144
145         ChangeTable::iterator it = table_.begin();
146
147         for (; it != table_.end(); ) {
148                 // current change starts like or follows new change
149                 if (it->range.start >= start) {
150                         break;
151                 }
152
153                 // new change intersects with existing change
154                 if (it->range.end > start) {
155                         pos_type oldEnd = it->range.end;
156                         it->range.end = start;
157
158                         LYXERR(Debug::CHANGES, "  cutting tail of type " << it->change.type
159                                 << " resulting in range (" << it->range.start << ", "
160                                 << it->range.end << ")");
161
162                         ++it;
163                         if (oldEnd >= end) {
164                                 LYXERR(Debug::CHANGES, "  inserting tail in range ("
165                                         << end << ", " << oldEnd << ")");
166                                 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
167                         }
168                         continue;
169                 }
170
171                 ++it;
172         }
173
174         if (change.type != Change::UNCHANGED) {
175                 LYXERR(Debug::CHANGES, "  inserting change");
176                 it = table_.insert(it, ChangeRange(change, Range(start, end)));
177                 ++it;
178         }
179
180         for (; it != table_.end(); ) {
181                 // new change 'contains' existing change
182                 if (newRange.contains(it->range)) {
183                         LYXERR(Debug::CHANGES, "  removing subrange ("
184                                 << it->range.start << ", " << it->range.end << ")");
185                         it = table_.erase(it);
186                         continue;
187                 }
188
189                 // new change precedes existing change
190                 if (it->range.start >= end)
191                         break;
192
193                 // new change intersects with existing change
194                 it->range.start = end;
195                 LYXERR(Debug::CHANGES, "  cutting head of type "
196                         << it->change.type << " resulting in range ("
197                         << end << ", " << it->range.end << ")");
198                 break; // no need for another iteration
199         }
200
201         merge();
202 }
203
204
205 void Changes::erase(pos_type const pos)
206 {
207         LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
208
209         ChangeTable::iterator it = table_.begin();
210         ChangeTable::iterator end = table_.end();
211
212         for (; it != end; ++it) {
213                 // range (pos,pos+x) becomes (pos,pos+x-1)
214                 if (it->range.start > pos)
215                         --(it->range.start);
216                 // range (pos-x,pos) stays (pos-x,pos)
217                 if (it->range.end > pos)
218                         --(it->range.end);
219         }
220
221         merge();
222 }
223
224
225 void Changes::insert(Change const & change, lyx::pos_type pos)
226 {
227         if (change.type != Change::UNCHANGED) {
228                 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
229                         << " at position " << pos);
230         }
231
232         ChangeTable::iterator it = table_.begin();
233         ChangeTable::iterator end = table_.end();
234
235         for (; it != end; ++it) {
236                 // range (pos,pos+x) becomes (pos+1,pos+x+1)
237                 if (it->range.start >= pos)
238                         ++(it->range.start);
239
240                 // range (pos-x,pos) stays as it is
241                 if (it->range.end > pos)
242                         ++(it->range.end);
243         }
244
245         set(change, pos, pos + 1); // set will call merge
246 }
247
248
249 Change const & Changes::lookup(pos_type const pos) const
250 {
251         static Change const noChange = Change(Change::UNCHANGED);
252
253         ChangeTable::const_iterator it = table_.begin();
254         ChangeTable::const_iterator const end = table_.end();
255
256         for (; it != end; ++it) {
257                 if (it->range.contains(pos))
258                         return it->change;
259         }
260
261         return noChange;
262 }
263
264
265 bool Changes::isDeleted(pos_type start, pos_type end) const
266 {
267         ChangeTable::const_iterator it = table_.begin();
268         ChangeTable::const_iterator const itend = table_.end();
269
270         for (; it != itend; ++it) {
271                 if (it->range.contains(Range(start, end))) {
272                         LYXERR(Debug::CHANGES, "range ("
273                                 << start << ", " << end << ") fully contains ("
274                                 << it->range.start << ", " << it->range.end
275                                 << ") of type " << it->change.type);
276                         return it->change.type == Change::DELETED;
277                 }
278         }
279         return false;
280 }
281
282
283 bool Changes::isChanged(pos_type const start, pos_type const end) const
284 {
285         ChangeTable::const_iterator it = table_.begin();
286         ChangeTable::const_iterator const itend = table_.end();
287
288         for (; it != itend; ++it) {
289                 if (it->range.intersects(Range(start, end))) {
290                         LYXERR(Debug::CHANGES, "found intersection of range ("
291                                 << start << ", " << end << ") with ("
292                                 << it->range.start << ", " << it->range.end
293                                 << ") of type " << it->change.type);
294                         return true;
295                 }
296         }
297         return false;
298 }
299
300
301 bool Changes::isChanged() const
302 {
303         ChangeTable::const_iterator it = table_.begin();
304         ChangeTable::const_iterator const itend = table_.end();
305         for (; it != itend; ++it) {
306                 if (it->change.changed())
307                         return true;
308         }
309         return false;
310 }
311
312
313 void Changes::merge()
314 {
315         bool merged = false;
316         ChangeTable::iterator it = table_.begin();
317
318         while (it != table_.end()) {
319                 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
320                         << " and range (" << it->range.start << ", " << it->range.end
321                         << ")");
322
323                 if (it->range.start == it->range.end) {
324                         LYXERR(Debug::CHANGES, "removing empty range for pos "
325                                 << it->range.start);
326
327                         table_.erase(it);
328                         merged = true;
329                         // start again
330                         it = table_.begin();
331                         continue;
332                 }
333
334                 if (it + 1 == table_.end())
335                         break;
336
337                 if (it->change.isSimilarTo((it + 1)->change)
338                     && it->range.end == (it + 1)->range.start) {
339                         LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
340                                 << it->range.end << ") and (" << (it + 1)->range.start << ", "
341                                 << (it + 1)->range.end << ")");
342
343                         (it + 1)->range.start = it->range.start;
344                         (it + 1)->change.changetime = max(it->change.changetime,
345                                                           (it + 1)->change.changetime);
346                         table_.erase(it);
347                         merged = true;
348                         // start again
349                         it = table_.begin();
350                         continue;
351                 }
352
353                 ++it;
354         }
355         if (merged && !isChanged())
356                 is_update_required_ = true;
357 }
358
359
360 namespace {
361
362 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
363                          docstring const & chgTime,
364                          OutputParams const & runparams)
365 {
366         if (macro.empty())
367                 return docstring();
368
369         docstring uncodable_author;
370         odocstringstream ods;
371
372         ods << macro;
373         // convert utf8 author name to something representable
374         // in the current encoding
375         pair<docstring, docstring> author_latexed =
376                 runparams.encoding->latexString(author, runparams.dryrun);
377         if (!author_latexed.second.empty()) {
378                 LYXERR0("Omitting uncodable characters '"
379                         << author_latexed.second
380                         << "' in change author name!");
381                 uncodable_author = author;
382         }
383         ods << author_latexed.first << "}{" << chgTime << "}{";
384
385         // warn user (once) if we found uncodable glyphs.
386         if (!uncodable_author.empty()) {
387                 static std::set<docstring> warned_authors;
388                 static Mutex warned_mutex;
389                 Mutex::Locker locker(&warned_mutex);
390                 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
391                         frontend::Alert::warning(_("Uncodable character in author name"),
392                                 support::bformat(_("The author name '%1$s',\n"
393                                   "used for change tracking, contains the following glyphs that\n"
394                                   "cannot be represented in the current encoding: %2$s.\n"
395                                   "These glyphs will be omitted in the exported LaTeX file.\n\n"
396                                   "Choose an appropriate document encoding (such as utf8)\n"
397                                   "or change the spelling of the author name."),
398                                 uncodable_author, author_latexed.second));
399                         warned_authors.insert(uncodable_author);
400                 }
401         }
402
403         return ods.str();
404 }
405
406 } //namespace anon
407
408
409 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
410                              Change const & oldChange, Change const & change,
411                              OutputParams const & runparams)
412 {
413         if (!bparams.output_changes || oldChange == change)
414                 return 0;
415
416         int column = 0;
417
418         if (oldChange.type != Change::UNCHANGED) {
419                 // close \lyxadded or \lyxdeleted
420                 os << '}';
421                 column++;
422                 if (oldChange.type == Change::DELETED)
423                         --runparams.inulemcmd;
424         }
425
426         docstring chgTime;
427         chgTime += asctime(gmtime(&change.changetime));
428         // remove trailing '\n'
429         chgTime.erase(chgTime.end() - 1);
430
431         docstring macro_beg;
432         if (change.type == Change::DELETED) {
433                 macro_beg = from_ascii("\\lyxdeleted{");
434                 ++runparams.inulemcmd;
435         }
436         else if (change.type == Change::INSERTED)
437                 macro_beg = from_ascii("\\lyxadded{");
438         
439         docstring str = getLaTeXMarkup(macro_beg,
440                                        bparams.authors().get(change.author).name(),
441                                        chgTime, runparams);
442         
443         os << str;
444         column += str.size();
445
446         return column;
447 }
448
449
450 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
451                             Change const & old, Change const & change)
452 {
453         if (old == change)
454                 return;
455
456         column = 0;
457
458         int const buffer_id = bparams.authors().get(change.author).bufferId();
459
460         switch (change.type) {
461                 case Change::UNCHANGED:
462                         os << "\n\\change_unchanged\n";
463                         break;
464
465                 case Change::DELETED:
466                         os << "\n\\change_deleted " << buffer_id
467                                 << " " << change.changetime << "\n";
468                         break;
469
470                 case Change::INSERTED:
471                         os << "\n\\change_inserted " << buffer_id
472                                 << " " << change.changetime << "\n";
473                         break;
474         }
475 }
476
477
478 void Changes::checkAuthors(AuthorList const & authorList)
479 {
480         ChangeTable::const_iterator it = table_.begin();
481         ChangeTable::const_iterator endit = table_.end();
482         for ( ; it != endit ; ++it) 
483                 if (it->change.type != Change::UNCHANGED)
484                         authorList.get(it->change.author).setUsed(true);
485 }
486
487
488 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
489         bool output_active) const
490 {
491         if (table_.empty())
492                 return;
493
494         shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
495         AuthorList const & author_list = buffer.params().authors();
496         DocIterator dit = cdit;
497
498         ChangeTable::const_iterator it = table_.begin();
499         ChangeTable::const_iterator const itend = table_.end();
500         for (; it != itend; ++it) {
501                 docstring str;
502                 switch (it->change.type) {
503                 case Change::UNCHANGED:
504                         continue;
505                 case Change::DELETED:
506                         // ✂ U+2702 BLACK SCISSORS
507                         str.push_back(0x2702);
508                         break;
509                 case Change::INSERTED:
510                         // ✍ U+270D WRITING HAND
511                         str.push_back(0x270d);
512                         break;
513                 }
514                 dit.pos() = it->range.start;
515                 Paragraph const & par = dit.paragraph();
516                 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
517                 if (it->range.end > par.size())
518                         // ¶ U+00B6 PILCROW SIGN
519                         str.push_back(0xb6);
520                 docstring const & author = author_list.get(it->change.author).name();
521                 Toc::iterator it = change_list->item(0, author);
522                 if (it == change_list->end()) {
523                         change_list->push_back(TocItem(dit, 0, author, true));
524                         change_list->push_back(TocItem(dit, 1, str, output_active,
525                                 support::wrapParas(str, 4)));
526                         continue;
527                 }
528                 for (++it; it != change_list->end(); ++it) {
529                         if (it->depth() == 0 && it->str() != author)
530                                 break;
531                 }
532                 change_list->insert(it, TocItem(dit, 1, str, output_active,
533                         support::wrapParas(str, 4)));
534         }
535 }
536
537
538 void Changes::updateBuffer(Buffer const & buf)
539 {
540         is_update_required_ = false;
541         if (!buf.areChangesPresent() && isChanged())
542                 buf.setChangesPresent(true);
543 }
544
545
546
547
548 } // namespace lyx