]> git.lyx.org Git - lyx.git/blob - src/Changes.cpp
Fix bug #11398
[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 "MetricsInfo.h"
23 #include "OutputParams.h"
24 #include "Paragraph.h"
25 #include "texstream.h"
26 #include "TocBackend.h"
27
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"
33
34 #include "frontends/alert.h"
35 #include "frontends/FontMetrics.h"
36 #include "frontends/Painter.h"
37
38 #include <ostream>
39
40 using namespace std;
41
42 namespace lyx {
43
44 using frontend::Painter;
45 using frontend::FontMetrics;
46
47 /*
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
51  * export.
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.
56  */
57
58 bool Change::isSimilarTo(Change const & change) const
59 {
60         if (type != change.type)
61                 return false;
62
63         if (type == Change::UNCHANGED)
64                 return true;
65
66         return author == change.author;
67 }
68
69
70 Color Change::color() const
71 {
72         Color color = Color_none;
73         switch (author % 5) {
74                 case 0:
75                         color = Color_changedtextauthor1;
76                         break;
77                 case 1:
78                         color = Color_changedtextauthor2;
79                         break;
80                 case 2:
81                         color = Color_changedtextauthor3;
82                         break;
83                 case 3:
84                         color = Color_changedtextauthor4;
85                         break;
86                 case 4:
87                         color = Color_changedtextauthor5;
88                         break;
89         }
90
91         if (deleted())
92                 color.mergeColor = Color_deletedtextmodifier;
93
94         return color;
95 }
96
97
98 bool operator==(Change const & l, Change const & r)
99 {
100         if (l.type != r.type)
101                 return false;
102
103         // two changes of type UNCHANGED are always equal
104         if (l.type == Change::UNCHANGED)
105                 return true;
106
107         return l.author == r.author && l.changetime == r.changetime;
108 }
109
110
111 bool operator!=(Change const & l, Change const & r)
112 {
113         return !(l == r);
114 }
115
116
117 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
118 {
119         return r1.start == r2.start && r1.end == r2.end;
120 }
121
122
123 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
124 {
125         return !(r1 == r2);
126 }
127
128
129 bool Changes::Range::intersects(Range const & r) const
130 {
131         return r.start < end && r.end > start; // end itself is not in the range!
132 }
133
134
135 void Changes::set(Change const & change, pos_type const pos)
136 {
137         set(change, pos, pos + 1);
138 }
139
140
141 void Changes::set(Change const & change, pos_type const start, pos_type const end)
142 {
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 << ")");
148         }
149
150         Range const newRange(start, end);
151
152         ChangeTable::iterator it = table_.begin();
153
154         for (; it != table_.end(); ) {
155                 // current change starts like or follows new change
156                 if (it->range.start >= start) {
157                         break;
158                 }
159
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;
164
165                         LYXERR(Debug::CHANGES, "  cutting tail of type " << it->change.type
166                                 << " resulting in range (" << it->range.start << ", "
167                                 << it->range.end << ")");
168
169                         ++it;
170                         if (oldEnd >= end) {
171                                 LYXERR(Debug::CHANGES, "  inserting tail in range ("
172                                         << end << ", " << oldEnd << ")");
173                                 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
174                         }
175                         continue;
176                 }
177
178                 ++it;
179         }
180
181         if (change.type != Change::UNCHANGED) {
182                 LYXERR(Debug::CHANGES, "  inserting change");
183                 it = table_.insert(it, ChangeRange(change, Range(start, end)));
184                 ++it;
185         }
186
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);
193                         continue;
194                 }
195
196                 // new change precedes existing change
197                 if (it->range.start >= end)
198                         break;
199
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
206         }
207
208         merge();
209 }
210
211
212 void Changes::erase(pos_type const pos)
213 {
214         LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
215
216         for (ChangeRange & cr : table_) {
217                 // range (pos,pos+x) becomes (pos,pos+x-1)
218                 if (cr.range.start > pos)
219                         --(cr.range.start);
220                 // range (pos-x,pos) stays (pos-x,pos)
221                 if (cr.range.end > pos)
222                         --(cr.range.end);
223         }
224
225         merge();
226 }
227
228
229 void Changes::insert(Change const & change, lyx::pos_type pos)
230 {
231         if (change.type != Change::UNCHANGED) {
232                 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
233                         << " at position " << pos);
234         }
235
236         for (ChangeRange & cr : table_) {
237                 // range (pos,pos+x) becomes (pos+1,pos+x+1)
238                 if (cr.range.start >= pos)
239                         ++(cr.range.start);
240
241                 // range (pos-x,pos) stays as it is
242                 if (cr.range.end > pos)
243                         ++(cr.range.end);
244         }
245
246         set(change, pos, pos + 1); // set will call merge
247 }
248
249
250 Change const & Changes::lookup(pos_type const pos) const
251 {
252         static Change const noChange = Change(Change::UNCHANGED);
253         for (ChangeRange const & cr : table_)
254                 if (cr.range.contains(pos))
255                         return cr.change;
256         return noChange;
257 }
258
259
260 bool Changes::isDeleted(pos_type start, pos_type end) const
261 {
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;
269                 }
270         return false;
271 }
272
273
274 bool Changes::isChanged(pos_type const start, pos_type const end) const
275 {
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);
282                         return true;
283                 }
284         return false;
285 }
286
287
288 bool Changes::isChanged() const
289 {
290         for (ChangeRange const & cr : table_)
291                 if (cr.change.changed())
292                         return true;
293         return false;
294 }
295
296
297 void Changes::merge()
298 {
299         ChangeTable::iterator it = table_.begin();
300
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
304                         << ")");
305
306                 if (it->range.start == it->range.end) {
307                         LYXERR(Debug::CHANGES, "removing empty range for pos "
308                                 << it->range.start);
309
310                         table_.erase(it);
311                         // start again
312                         it = table_.begin();
313                         continue;
314                 }
315
316                 if (it + 1 == table_.end())
317                         break;
318
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 << ")");
324
325                         (it + 1)->range.start = it->range.start;
326                         (it + 1)->change.changetime = max(it->change.changetime,
327                                                           (it + 1)->change.changetime);
328                         table_.erase(it);
329                         // start again
330                         it = table_.begin();
331                         continue;
332                 }
333
334                 ++it;
335         }
336 }
337
338
339 namespace {
340
341 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
342                          docstring const & chgTime,
343                          OutputParams const & runparams)
344 {
345         if (macro.empty())
346                 return docstring();
347
348         docstring uncodable_author;
349         odocstringstream ods;
350
351         ods << macro;
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;
361         }
362         ods << author_latexed.first << "}{" << chgTime << "}{";
363
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);
379                 }
380         }
381
382         return ods.str();
383 }
384
385 } // namespace
386
387
388 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
389                              Change const & oldChange, Change const & change,
390                              OutputParams const & runparams)
391 {
392         if (!bparams.output_changes || oldChange == change)
393                 return 0;
394
395         int column = 0;
396
397         bool const dvipost = LaTeXFeatures::isAvailable("dvipost") &&
398                         (runparams.flavor == OutputParams::LATEX
399                          || runparams.flavor == OutputParams::DVILUATEX);
400
401         if (oldChange.type != Change::UNCHANGED) {
402                 // close \lyxadded or \lyxdeleted
403                 os << '}';
404                 column++;
405                 if (oldChange.type == Change::DELETED
406                     && !runparams.wasDisplayMath && !dvipost)
407                         --runparams.inulemcmd;
408         }
409
410         docstring chgTime;
411         chgTime += asctime(gmtime(&change.changetime));
412         // remove trailing '\n'
413         chgTime.erase(chgTime.end() - 1);
414
415         docstring macro_beg;
416         if (change.type == Change::DELETED) {
417                 macro_beg = from_ascii("\\lyxdeleted{");
418                 if (!runparams.inDisplayMath && !dvipost)
419                         ++runparams.inulemcmd;
420         }
421         else if (change.type == Change::INSERTED)
422                 macro_beg = from_ascii("\\lyxadded{");
423
424         docstring str = getLaTeXMarkup(macro_beg,
425                                        bparams.authors().get(change.author).name(),
426                                        chgTime, runparams);
427
428         // signature needed by \lyxsout to correctly strike out display math
429         if (change.type == Change::DELETED && runparams.inDisplayMath
430             && !dvipost) {
431                 if (os.lastChar() == '\n')
432                         str += from_ascii("\\\\\\noindent\n");
433                 else
434                         str += from_ascii("\\\\\\\\\n");
435         }
436
437         os << str;
438         column += str.size();
439
440         return column;
441 }
442
443
444 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
445                             Change const & old, Change const & change)
446 {
447         if (old == change)
448                 return;
449
450         column = 0;
451
452         int const buffer_id = bparams.authors().get(change.author).bufferId();
453
454         switch (change.type) {
455                 case Change::UNCHANGED:
456                         os << "\n\\change_unchanged\n";
457                         break;
458
459                 case Change::DELETED:
460                         os << "\n\\change_deleted " << buffer_id
461                                 << " " << change.changetime << "\n";
462                         break;
463
464                 case Change::INSERTED:
465                         os << "\n\\change_inserted " << buffer_id
466                                 << " " << change.changetime << "\n";
467                         break;
468         }
469 }
470
471
472 void Changes::checkAuthors(AuthorList const & authorList)
473 {
474         for (ChangeRange const & cr : table_)
475                 if (cr.change.type != Change::UNCHANGED)
476                         authorList.get(cr.change.author).setUsed(true);
477 }
478
479
480 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
481                        bool output_active, TocBackend & backend) const
482 {
483         if (table_.empty())
484                 return;
485
486         shared_ptr<Toc> change_list = backend.toc("change");
487         AuthorList const & author_list = buffer.params().authors();
488         DocIterator dit = cdit;
489
490         for (ChangeRange const & cr : table_) {
491                 docstring str;
492                 switch (cr.change.type) {
493                 case Change::UNCHANGED:
494                         continue;
495                 case Change::DELETED:
496                         // ✂ U+2702 BLACK SCISSORS
497                         str.push_back(0x2702);
498                         break;
499                 case Change::INSERTED:
500                         // ✍ U+270D WRITING HAND
501                         str.push_back(0x270d);
502                         break;
503                 }
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
509                         str.push_back(0xb6);
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));
515                         continue;
516                 }
517                 for (++it; it != change_list->end(); ++it) {
518                         if (it->depth() == 0 && it->str() != author)
519                                 break;
520                 }
521                 change_list->insert(it, TocItem(dit, 1, str, output_active));
522         }
523 }
524
525
526 void Changes::updateBuffer(Buffer const & buf)
527 {
528         bool const changed = isChanged();
529         buf.setChangesPresent(buf.areChangesPresent() || changed);
530         previously_changed_ = changed;
531 }
532
533
534 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
535                       double const x2, FontInfo const & font) const
536 {
537         if (!changed())
538                 return;
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());
545 }
546
547
548 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
549                       double const x2, double const y2) const
550 {
551         /*
552          * y1      /
553          *        /
554          *       /
555          *      /
556          *     /
557          * y2 /_____
558          *    x1  x2
559          */
560         switch(type) {
561         case UNCHANGED:
562                 return;
563         case INSERTED:
564                 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
565                              color(), Painter::line_solid,
566                              pi.base.solidLineThickness());
567                 return;
568         case DELETED:
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());
574                 return;
575         }
576 }
577
578
579 } // namespace lyx