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