]> git.lyx.org Git - lyx.git/blob - src/Changes.cpp
Remove updateInfo() calls in favor of doing the relevant work
[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         if (oldChange.type != Change::UNCHANGED) {
398                 // close \lyxadded or \lyxdeleted
399                 os << '}';
400                 column++;
401                 if (oldChange.type == Change::DELETED && !runparams.wasDisplayMath)
402                         --runparams.inulemcmd;
403         }
404
405         docstring chgTime;
406         chgTime += asctime(gmtime(&change.changetime));
407         // remove trailing '\n'
408         chgTime.erase(chgTime.end() - 1);
409
410         docstring macro_beg;
411         if (change.type == Change::DELETED) {
412                 macro_beg = from_ascii("\\lyxdeleted{");
413                 if (!runparams.inDisplayMath)
414                         ++runparams.inulemcmd;
415         }
416         else if (change.type == Change::INSERTED)
417                 macro_beg = from_ascii("\\lyxadded{");
418
419         docstring str = getLaTeXMarkup(macro_beg,
420                                        bparams.authors().get(change.author).name(),
421                                        chgTime, runparams);
422
423         // signature needed by \lyxsout to correctly strike out display math
424         if (change.type == Change::DELETED && runparams.inDisplayMath
425             && (!LaTeXFeatures::isAvailable("dvipost")
426                 || (runparams.flavor != OutputParams::LATEX
427                     && runparams.flavor != OutputParams::DVILUATEX))) {
428                 if (os.afterParbreak())
429                         str += from_ascii("\\\\\\noindent\n");
430                 else
431                         str += from_ascii("\\\\\\\\\n");
432         }
433
434         os << str;
435         column += str.size();
436
437         return column;
438 }
439
440
441 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
442                             Change const & old, Change const & change)
443 {
444         if (old == change)
445                 return;
446
447         column = 0;
448
449         int const buffer_id = bparams.authors().get(change.author).bufferId();
450
451         switch (change.type) {
452                 case Change::UNCHANGED:
453                         os << "\n\\change_unchanged\n";
454                         break;
455
456                 case Change::DELETED:
457                         os << "\n\\change_deleted " << buffer_id
458                                 << " " << change.changetime << "\n";
459                         break;
460
461                 case Change::INSERTED:
462                         os << "\n\\change_inserted " << buffer_id
463                                 << " " << change.changetime << "\n";
464                         break;
465         }
466 }
467
468
469 void Changes::checkAuthors(AuthorList const & authorList)
470 {
471         for (ChangeRange const & cr : table_)
472                 if (cr.change.type != Change::UNCHANGED)
473                         authorList.get(cr.change.author).setUsed(true);
474 }
475
476
477 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
478                        bool output_active, TocBackend & backend) const
479 {
480         if (table_.empty())
481                 return;
482
483         shared_ptr<Toc> change_list = backend.toc("change");
484         AuthorList const & author_list = buffer.params().authors();
485         DocIterator dit = cdit;
486
487         for (ChangeRange const & cr : table_) {
488                 docstring str;
489                 switch (cr.change.type) {
490                 case Change::UNCHANGED:
491                         continue;
492                 case Change::DELETED:
493                         // ✂ U+2702 BLACK SCISSORS
494                         str.push_back(0x2702);
495                         break;
496                 case Change::INSERTED:
497                         // ✍ U+270D WRITING HAND
498                         str.push_back(0x270d);
499                         break;
500                 }
501                 dit.pos() = cr.range.start;
502                 Paragraph const & par = dit.paragraph();
503                 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
504                 if (cr.range.end > par.size())
505                         // ¶ U+00B6 PILCROW SIGN
506                         str.push_back(0xb6);
507                 docstring const & author = author_list.get(cr.change.author).name();
508                 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
509                 if (it == change_list->end()) {
510                         change_list->push_back(TocItem(dit, 0, author, true));
511                         change_list->push_back(TocItem(dit, 1, str, output_active));
512                         continue;
513                 }
514                 for (++it; it != change_list->end(); ++it) {
515                         if (it->depth() == 0 && it->str() != author)
516                                 break;
517                 }
518                 change_list->insert(it, TocItem(dit, 1, str, output_active));
519         }
520 }
521
522
523 void Changes::updateBuffer(Buffer const & buf)
524 {
525         bool const changed = isChanged();
526         buf.setChangesPresent(buf.areChangesPresent() || changed);
527         previously_changed_ = changed;
528 }
529
530
531 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
532                       double const x2, FontInfo const & font) const
533 {
534         if (!changed())
535                 return;
536         // Calculate 1/3 height of font
537         FontMetrics const & fm = theFontMetrics(font);
538         double const y_bar = deleted() ? y - fm.maxAscent() / 3
539                 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
540         pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
541                      Painter::line_solid, pi.base.solidLineThickness());
542 }
543
544
545 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
546                       double const x2, double const y2) const
547 {
548         /*
549          * y1      /
550          *        /
551          *       /
552          *      /
553          *     /
554          * y2 /_____
555          *    x1  x2
556          */
557         switch(type) {
558         case UNCHANGED:
559                 return;
560         case INSERTED:
561                 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
562                              color(), Painter::line_solid,
563                              pi.base.solidLineThickness());
564                 return;
565         case DELETED:
566                 // FIXME: we cannot use antialias since we keep drawing on the same
567                 // background with the current painting mechanism.
568                 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
569                              color(), Painter::line_solid_aliased,
570                              pi.base.solidLineThickness());
571                 return;
572         }
573 }
574
575
576 } // namespace lyx