]> git.lyx.org Git - lyx.git/blob - src/Changes.cpp
Revert "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 "LyXRC.h"
23 #include "MetricsInfo.h"
24 #include "OutputParams.h"
25 #include "Paragraph.h"
26 #include "texstream.h"
27 #include "TocBackend.h"
28
29 #include "support/debug.h"
30 #include "support/gettext.h"
31 #include "support/lassert.h"
32 #include "support/lstrings.h"
33 #include "support/mutex.h"
34
35 #include "frontends/alert.h"
36 #include "frontends/FontMetrics.h"
37 #include "frontends/Painter.h"
38
39 #include <ostream>
40
41 using namespace std;
42
43 namespace lyx {
44
45 using frontend::Painter;
46 using frontend::FontMetrics;
47
48 /*
49  * Class Change has a changetime field that specifies the exact time at which
50  * a specific change was made. The change time is used as a guidance for the
51  * user while editing his document. Presently, it is not considered for LaTeX
52  * export.
53  * When merging two adjacent changes, the changetime is not considered,
54  * only the equality of the change type and author is checked (in method
55  * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
56  * the later change time is preserved.
57  */
58
59 bool Change::isSimilarTo(Change const & change) const
60 {
61         if (type != change.type)
62                 return false;
63
64         if (type == Change::UNCHANGED)
65                 return true;
66
67         return author == change.author;
68 }
69
70
71 Color Change::color() const
72 {
73         Color color = Color_none;
74         switch (author % 5) {
75                 case 0:
76                         color = Color_addedtextauthor1;
77                         break;
78                 case 1:
79                         color = Color_addedtextauthor2;
80                         break;
81                 case 2:
82                         color = Color_addedtextauthor3;
83                         break;
84                 case 3:
85                         color = Color_addedtextauthor4;
86                         break;
87                 case 4:
88                         color = Color_addedtextauthor5;
89                         break;
90         }
91
92         if (deleted())
93                 color.mergeColor = Color_deletedtextmodifier;
94
95         return color;
96 }
97
98
99 bool operator==(Change const & l, Change const & r)
100 {
101         if (l.type != r.type)
102                 return false;
103
104         // two changes of type UNCHANGED are always equal
105         if (l.type == Change::UNCHANGED)
106                 return true;
107
108         return l.author == r.author && l.changetime == r.changetime;
109 }
110
111
112 bool operator!=(Change const & l, Change const & r)
113 {
114         return !(l == r);
115 }
116
117
118 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
119 {
120         return r1.start == r2.start && r1.end == r2.end;
121 }
122
123
124 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
125 {
126         return !(r1 == r2);
127 }
128
129
130 bool Changes::Range::intersects(Range const & r) const
131 {
132         return r.start < end && r.end > start; // end itself is not in the range!
133 }
134
135
136 void Changes::set(Change const & change, pos_type const pos)
137 {
138         set(change, pos, pos + 1);
139 }
140
141
142 void Changes::set(Change const & change, pos_type const start, pos_type const end)
143 {
144         if (change.type != Change::UNCHANGED) {
145                 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
146                         << ", author: " << change.author
147                         << ", time: " << long(change.changetime)
148                         << ") in range (" << start << ", " << end << ")");
149         }
150
151         Range const newRange(start, end);
152
153         ChangeTable::iterator it = table_.begin();
154
155         for (; it != table_.end(); ) {
156                 // current change starts like or follows new change
157                 if (it->range.start >= start) {
158                         break;
159                 }
160
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;
165
166                         LYXERR(Debug::CHANGES, "  cutting tail of type " << it->change.type
167                                 << " resulting in range (" << it->range.start << ", "
168                                 << it->range.end << ")");
169
170                         ++it;
171                         if (oldEnd >= end) {
172                                 LYXERR(Debug::CHANGES, "  inserting tail in range ("
173                                         << end << ", " << oldEnd << ")");
174                                 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
175                         }
176                         continue;
177                 }
178
179                 ++it;
180         }
181
182         if (change.type != Change::UNCHANGED) {
183                 LYXERR(Debug::CHANGES, "  inserting change");
184                 it = table_.insert(it, ChangeRange(change, Range(start, end)));
185                 ++it;
186         }
187
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);
194                         continue;
195                 }
196
197                 // new change precedes existing change
198                 if (it->range.start >= end)
199                         break;
200
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
207         }
208
209         merge();
210 }
211
212
213 void Changes::erase(pos_type const pos)
214 {
215         LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
216
217         for (ChangeRange & cr : table_) {
218                 // range (pos,pos+x) becomes (pos,pos+x-1)
219                 if (cr.range.start > pos)
220                         --(cr.range.start);
221                 // range (pos-x,pos) stays (pos-x,pos)
222                 if (cr.range.end > pos)
223                         --(cr.range.end);
224         }
225
226         merge();
227 }
228
229
230 void Changes::insert(Change const & change, lyx::pos_type pos)
231 {
232         if (change.type != Change::UNCHANGED) {
233                 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
234                         << " at position " << pos);
235         }
236
237         for (ChangeRange & cr : table_) {
238                 // range (pos,pos+x) becomes (pos+1,pos+x+1)
239                 if (cr.range.start >= pos)
240                         ++(cr.range.start);
241
242                 // range (pos-x,pos) stays as it is
243                 if (cr.range.end > pos)
244                         ++(cr.range.end);
245         }
246
247         set(change, pos, pos + 1); // set will call merge
248 }
249
250
251 Change const & Changes::lookup(pos_type const pos) const
252 {
253         static Change const noChange = Change(Change::UNCHANGED);
254         for (ChangeRange const & cr : table_)
255                 if (cr.range.contains(pos))
256                         return cr.change;
257         return noChange;
258 }
259
260
261 bool Changes::isDeleted(pos_type start, pos_type end) const
262 {
263         for (ChangeRange const & cr : table_)
264                 if (cr.range.contains(Range(start, end))) {
265                         LYXERR(Debug::CHANGES, "range ("
266                                 << start << ", " << end << ") fully contains ("
267                                 << cr.range.start << ", " << cr.range.end
268                                 << ") of type " << cr.change.type);
269                         return cr.change.type == Change::DELETED;
270                 }
271         return false;
272 }
273
274
275 bool Changes::isChanged(pos_type const start, pos_type const end) const
276 {
277         for (ChangeRange const & cr : table_)
278                 if (cr.range.intersects(Range(start, end))) {
279                         LYXERR(Debug::CHANGES, "found intersection of range ("
280                                 << start << ", " << end << ") with ("
281                                 << cr.range.start << ", " << cr.range.end
282                                 << ") of type " << cr.change.type);
283                         return true;
284                 }
285         return false;
286 }
287
288
289 bool Changes::isChanged() const
290 {
291         for (ChangeRange const & cr : table_)
292                 if (cr.change.changed())
293                         return true;
294         return false;
295 }
296
297
298 void Changes::merge()
299 {
300         ChangeTable::iterator it = table_.begin();
301
302         while (it != table_.end()) {
303                 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
304                         << " and range (" << it->range.start << ", " << it->range.end
305                         << ")");
306
307                 if (it->range.start == it->range.end) {
308                         LYXERR(Debug::CHANGES, "removing empty range for pos "
309                                 << it->range.start);
310
311                         table_.erase(it);
312                         // start again
313                         it = table_.begin();
314                         continue;
315                 }
316
317                 if (it + 1 == table_.end())
318                         break;
319
320                 if (it->change.isSimilarTo((it + 1)->change)
321                     && it->range.end == (it + 1)->range.start) {
322                         LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
323                                 << it->range.end << ") and (" << (it + 1)->range.start << ", "
324                                 << (it + 1)->range.end << ")");
325
326                         (it + 1)->range.start = it->range.start;
327                         (it + 1)->change.changetime = max(it->change.changetime,
328                                                           (it + 1)->change.changetime);
329                         table_.erase(it);
330                         // start again
331                         it = table_.begin();
332                         continue;
333                 }
334
335                 ++it;
336         }
337 }
338
339
340 namespace {
341
342 docstring getLaTeXMarkup(docstring const & macro, Author const & author,
343                          docstring const & chgTime,
344                          OutputParams const & runparams)
345 {
346         if (macro.empty())
347                 return docstring();
348
349         docstring uncodable_author;
350         odocstringstream ods;
351
352         docstring const author_name = author.name();
353         docstring const author_initials = author.initials();
354         
355         ods << macro;
356         if (!author_initials.empty()) {
357                 docstring uncodable_initials;
358                 // convert utf8 author initials to something representable
359                 // in the current encoding
360                 pair<docstring, docstring> author_initials_latexed =
361                         runparams.encoding->latexString(author_initials, runparams.dryrun);
362                 if (!author_initials_latexed.second.empty()) {
363                         LYXERR0("Omitting uncodable characters '"
364                                 << author_initials_latexed.second
365                                 << "' in change author initials!");
366                         uncodable_initials = author_initials;
367                 }
368                 ods << "[" << author_initials_latexed.first << "]";
369                 // warn user (once) if we found uncodable glyphs.
370                 if (!uncodable_initials.empty()) {
371                         static std::set<docstring> warned_author_initials;
372                         static Mutex warned_mutex;
373                         Mutex::Locker locker(&warned_mutex);
374                         if (warned_author_initials.find(uncodable_initials) == warned_author_initials.end()) {
375                                 frontend::Alert::warning(_("Uncodable character in author initials"),
376                                         support::bformat(_("The author initials '%1$s',\n"
377                                           "used for change tracking, contain the following glyphs that\n"
378                                           "cannot be represented in the current encoding: %2$s.\n"
379                                           "These glyphs will be omitted in the exported LaTeX file.\n\n"
380                                           "Choose an appropriate document encoding (such as utf8)\n"
381                                           "or change the author initials."),
382                                         uncodable_initials, author_initials_latexed.second));
383                                 warned_author_initials.insert(uncodable_initials);
384                         }
385                 }
386         }
387         // convert utf8 author name to something representable
388         // in the current encoding
389         pair<docstring, docstring> author_latexed =
390                 runparams.encoding->latexString(author_name, runparams.dryrun);
391         if (!author_latexed.second.empty()) {
392                 LYXERR0("Omitting uncodable characters '"
393                         << author_latexed.second
394                         << "' in change author name!");
395                 uncodable_author = author_name;
396         }
397         ods << "{" << author_latexed.first << "}{" << chgTime << "}{";
398
399         // warn user (once) if we found uncodable glyphs.
400         if (!uncodable_author.empty()) {
401                 static std::set<docstring> warned_authors;
402                 static Mutex warned_mutex;
403                 Mutex::Locker locker(&warned_mutex);
404                 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
405                         frontend::Alert::warning(_("Uncodable character in author name"),
406                                 support::bformat(_("The author name '%1$s',\n"
407                                   "used for change tracking, contains the following glyphs that\n"
408                                   "cannot be represented in the current encoding: %2$s.\n"
409                                   "These glyphs will be omitted in the exported LaTeX file.\n\n"
410                                   "Choose an appropriate document encoding (such as utf8)\n"
411                                   "or change the spelling of the author name."),
412                                 uncodable_author, author_latexed.second));
413                         warned_authors.insert(uncodable_author);
414                 }
415         }
416
417         return ods.str();
418 }
419
420 } // namespace
421
422
423 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
424                              Change const & oldChange, Change const & change,
425                              OutputParams const & runparams)
426 {
427         if (!bparams.output_changes || oldChange == change)
428                 return 0;
429
430         int column = 0;
431
432         bool const dvipost = LaTeXFeatures::isAvailable("dvipost") &&
433                         (runparams.flavor == OutputParams::LATEX
434                          || runparams.flavor == OutputParams::DVILUATEX);
435
436         if (oldChange.type != Change::UNCHANGED) {
437                 if (oldChange.type != Change::DELETED || runparams.ctObject != OutputParams::CT_OMITOBJECT) {
438                         // close \lyxadded or \lyxdeleted
439                         os << '}';
440                         column++;
441                 }
442                 if (oldChange.type == Change::DELETED
443                     && !runparams.wasDisplayMath && !dvipost)
444                         --runparams.inulemcmd;
445         }
446
447         docstring chgTime;
448         chgTime += asctime(gmtime(&change.changetime));
449         // remove trailing '\n'
450         chgTime.erase(chgTime.end() - 1);
451
452         docstring macro_beg;
453         if (change.type == Change::DELETED) {
454                 if (runparams.ctObject == OutputParams::CT_OMITOBJECT)
455                         return 0;
456                 else if (runparams.ctObject == OutputParams::CT_OBJECT)
457                         macro_beg = from_ascii("\\lyxobjdeleted");
458                 else if (runparams.ctObject == OutputParams::CT_DISPLAYOBJECT)
459                         macro_beg = from_ascii("\\lyxdisplayobjdeleted");
460                 else if (runparams.ctObject == OutputParams::CT_UDISPLAYOBJECT)
461                         macro_beg = from_ascii("\\lyxudisplayobjdeleted");
462                 else {
463                         macro_beg = from_ascii("\\lyxdeleted");
464                         if (!runparams.inDisplayMath && !dvipost)
465                                 ++runparams.inulemcmd;
466                 }
467         }
468         else if (change.type == Change::INSERTED)
469                 macro_beg = from_ascii("\\lyxadded");
470
471         docstring str = getLaTeXMarkup(macro_beg,
472                                        bparams.authors().get(change.author),
473                                        chgTime, runparams);
474
475         os << str;
476         column += str.size();
477
478         return column;
479 }
480
481
482 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
483                             Change const & old, Change const & change)
484 {
485         if (old == change)
486                 return;
487
488         column = 0;
489
490         int const buffer_id = bparams.authors().get(change.author).bufferId();
491
492         switch (change.type) {
493                 case Change::UNCHANGED:
494                         os << "\n\\change_unchanged\n";
495                         break;
496
497                 case Change::DELETED:
498                         os << "\n\\change_deleted " << buffer_id
499                                 << " " << change.changetime << "\n";
500                         break;
501
502                 case Change::INSERTED:
503                         os << "\n\\change_inserted " << buffer_id
504                                 << " " << change.changetime << "\n";
505                         break;
506         }
507 }
508
509
510 void Changes::checkAuthors(AuthorList const & authorList)
511 {
512         for (ChangeRange const & cr : table_)
513                 if (cr.change.type != Change::UNCHANGED)
514                         authorList.get(cr.change.author).setUsed(true);
515 }
516
517
518 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
519                        bool output_active, TocBackend & backend) const
520 {
521         if (table_.empty())
522                 return;
523
524         shared_ptr<Toc> change_list = backend.toc("change");
525         AuthorList const & author_list = buffer.params().authors();
526         DocIterator dit = cdit;
527
528         for (ChangeRange const & cr : table_) {
529                 docstring str;
530                 switch (cr.change.type) {
531                 case Change::UNCHANGED:
532                         continue;
533                 case Change::DELETED:
534                         // ✂ U+2702 BLACK SCISSORS
535                         str.push_back(0x2702);
536                         break;
537                 case Change::INSERTED:
538                         // ✍ U+270D WRITING HAND
539                         str.push_back(0x270d);
540                         break;
541                 }
542                 dit.pos() = cr.range.start;
543                 Paragraph const & par = dit.paragraph();
544                 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
545                 if (cr.range.end > par.size())
546                         // ¶ U+00B6 PILCROW SIGN
547                         str.push_back(0xb6);
548                 docstring const & author = author_list.get(cr.change.author).name();
549                 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
550                 if (it == change_list->end()) {
551                         change_list->push_back(TocItem(dit, 0, author, true));
552                         change_list->push_back(TocItem(dit, 1, str, output_active));
553                         continue;
554                 }
555                 for (++it; it != change_list->end(); ++it) {
556                         if (it->depth() == 0 && it->str() != author)
557                                 break;
558                 }
559                 change_list->insert(it, TocItem(dit, 1, str, output_active));
560         }
561 }
562
563
564 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
565                       double const x2, FontInfo const & font) const
566 {
567         if (!changed() || (!lyxrc.ct_additions_underlined && inserted()))
568                 return;
569         // Calculate 1/3 height of font
570         FontMetrics const & fm = theFontMetrics(font);
571         double const y_bar = deleted() ? y - fm.maxAscent() / 3
572                 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
573         pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
574                      Painter::line_solid, pi.base.solidLineThickness());
575 }
576
577
578 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
579                       double const x2, double const y2) const
580 {
581         /*
582          * y1      /
583          *        /
584          *       /
585          *      /
586          *     /
587          * y2 /_____
588          *    x1  x2
589          */
590         switch(type) {
591         case UNCHANGED:
592                 return;
593         case INSERTED: {
594                 if (!lyxrc.ct_additions_underlined)
595                         break;
596                 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
597                              color(), Painter::line_solid,
598                              pi.base.solidLineThickness());
599                 return;
600         }
601         case DELETED:
602                 // FIXME: we cannot use antialias since we keep drawing on the same
603                 // background with the current painting mechanism.
604                 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
605                              color(), Painter::line_solid_aliased,
606                              pi.base.solidLineThickness());
607                 return;
608         }
609 }
610
611
612 } // namespace lyx