]> git.lyx.org Git - lyx.git/blob - src/Changes.cpp
Disable changebar checkbox if show changes in output is off
[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_addedtextauthor1;
76                         break;
77                 case 1:
78                         color = Color_addedtextauthor2;
79                         break;
80                 case 2:
81                         color = Color_addedtextauthor3;
82                         break;
83                 case 3:
84                         color = Color_addedtextauthor4;
85                         break;
86                 case 4:
87                         color = Color_addedtextauthor5;
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, Author 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         docstring const author_name = author.name();
352         docstring const author_initials = author.initials();
353         
354         ods << macro;
355         if (!author_initials.empty()) {
356                 docstring uncodable_initials;
357                 // convert utf8 author initials to something representable
358                 // in the current encoding
359                 pair<docstring, docstring> author_initials_latexed =
360                         runparams.encoding->latexString(author_initials, runparams.dryrun);
361                 if (!author_initials_latexed.second.empty()) {
362                         LYXERR0("Omitting uncodable characters '"
363                                 << author_initials_latexed.second
364                                 << "' in change author initials!");
365                         uncodable_initials = author_initials;
366                 }
367                 ods << "[" << author_initials_latexed.first << "]";
368                 // warn user (once) if we found uncodable glyphs.
369                 if (!uncodable_initials.empty()) {
370                         static std::set<docstring> warned_author_initials;
371                         static Mutex warned_mutex;
372                         Mutex::Locker locker(&warned_mutex);
373                         if (warned_author_initials.find(uncodable_initials) == warned_author_initials.end()) {
374                                 frontend::Alert::warning(_("Uncodable character in author initials"),
375                                         support::bformat(_("The author initials '%1$s',\n"
376                                           "used for change tracking, contain the following glyphs that\n"
377                                           "cannot be represented in the current encoding: %2$s.\n"
378                                           "These glyphs will be omitted in the exported LaTeX file.\n\n"
379                                           "Choose an appropriate document encoding (such as utf8)\n"
380                                           "or change the author initials."),
381                                         uncodable_initials, author_initials_latexed.second));
382                                 warned_author_initials.insert(uncodable_author);
383                         }
384                 }
385         }
386         // convert utf8 author name to something representable
387         // in the current encoding
388         pair<docstring, docstring> author_latexed =
389                 runparams.encoding->latexString(author_name, runparams.dryrun);
390         if (!author_latexed.second.empty()) {
391                 LYXERR0("Omitting uncodable characters '"
392                         << author_latexed.second
393                         << "' in change author name!");
394                 uncodable_author = author_name;
395         }
396         ods << "{" << author_latexed.first << "}{" << chgTime << "}{";
397
398         // warn user (once) if we found uncodable glyphs.
399         if (!uncodable_author.empty()) {
400                 static std::set<docstring> warned_authors;
401                 static Mutex warned_mutex;
402                 Mutex::Locker locker(&warned_mutex);
403                 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
404                         frontend::Alert::warning(_("Uncodable character in author name"),
405                                 support::bformat(_("The author name '%1$s',\n"
406                                   "used for change tracking, contains the following glyphs that\n"
407                                   "cannot be represented in the current encoding: %2$s.\n"
408                                   "These glyphs will be omitted in the exported LaTeX file.\n\n"
409                                   "Choose an appropriate document encoding (such as utf8)\n"
410                                   "or change the spelling of the author name."),
411                                 uncodable_author, author_latexed.second));
412                         warned_authors.insert(uncodable_author);
413                 }
414         }
415
416         return ods.str();
417 }
418
419 } // namespace
420
421
422 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
423                              Change const & oldChange, Change const & change,
424                              OutputParams const & runparams)
425 {
426         if (!bparams.output_changes || oldChange == change)
427                 return 0;
428
429         int column = 0;
430
431         bool const dvipost = LaTeXFeatures::isAvailable("dvipost") &&
432                         (runparams.flavor == OutputParams::LATEX
433                          || runparams.flavor == OutputParams::DVILUATEX);
434
435         if (oldChange.type != Change::UNCHANGED) {
436                 // close \lyxadded or \lyxdeleted
437                 os << '}';
438                 column++;
439                 if (oldChange.type == Change::DELETED
440                     && !runparams.wasDisplayMath && !dvipost)
441                         --runparams.inulemcmd;
442         }
443
444         docstring chgTime;
445         chgTime += asctime(gmtime(&change.changetime));
446         // remove trailing '\n'
447         chgTime.erase(chgTime.end() - 1);
448
449         docstring macro_beg;
450         if (change.type == Change::DELETED) {
451                 macro_beg = from_ascii("\\lyxdeleted");
452                 if (!runparams.inDisplayMath && !dvipost)
453                         ++runparams.inulemcmd;
454         }
455         else if (change.type == Change::INSERTED)
456                 macro_beg = from_ascii("\\lyxadded");
457
458         docstring str = getLaTeXMarkup(macro_beg,
459                                        bparams.authors().get(change.author),
460                                        chgTime, runparams);
461
462         // signature needed by \lyxsout to correctly strike out display math
463         if (change.type == Change::DELETED && runparams.inDisplayMath
464             && !dvipost) {
465                 if (os.blankLine())
466                         str += from_ascii("\\\\\\noindent\n");
467                 else
468                         str += from_ascii("\\\\\\\\\n");
469         }
470
471         os << str;
472         column += str.size();
473
474         return column;
475 }
476
477
478 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
479                             Change const & old, Change const & change)
480 {
481         if (old == change)
482                 return;
483
484         column = 0;
485
486         int const buffer_id = bparams.authors().get(change.author).bufferId();
487
488         switch (change.type) {
489                 case Change::UNCHANGED:
490                         os << "\n\\change_unchanged\n";
491                         break;
492
493                 case Change::DELETED:
494                         os << "\n\\change_deleted " << buffer_id
495                                 << " " << change.changetime << "\n";
496                         break;
497
498                 case Change::INSERTED:
499                         os << "\n\\change_inserted " << buffer_id
500                                 << " " << change.changetime << "\n";
501                         break;
502         }
503 }
504
505
506 void Changes::checkAuthors(AuthorList const & authorList)
507 {
508         for (ChangeRange const & cr : table_)
509                 if (cr.change.type != Change::UNCHANGED)
510                         authorList.get(cr.change.author).setUsed(true);
511 }
512
513
514 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
515                        bool output_active, TocBackend & backend) const
516 {
517         if (table_.empty())
518                 return;
519
520         shared_ptr<Toc> change_list = backend.toc("change");
521         AuthorList const & author_list = buffer.params().authors();
522         DocIterator dit = cdit;
523
524         for (ChangeRange const & cr : table_) {
525                 docstring str;
526                 switch (cr.change.type) {
527                 case Change::UNCHANGED:
528                         continue;
529                 case Change::DELETED:
530                         // ✂ U+2702 BLACK SCISSORS
531                         str.push_back(0x2702);
532                         break;
533                 case Change::INSERTED:
534                         // ✍ U+270D WRITING HAND
535                         str.push_back(0x270d);
536                         break;
537                 }
538                 dit.pos() = cr.range.start;
539                 Paragraph const & par = dit.paragraph();
540                 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
541                 if (cr.range.end > par.size())
542                         // ¶ U+00B6 PILCROW SIGN
543                         str.push_back(0xb6);
544                 docstring const & author = author_list.get(cr.change.author).name();
545                 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
546                 if (it == change_list->end()) {
547                         change_list->push_back(TocItem(dit, 0, author, true));
548                         change_list->push_back(TocItem(dit, 1, str, output_active));
549                         continue;
550                 }
551                 for (++it; it != change_list->end(); ++it) {
552                         if (it->depth() == 0 && it->str() != author)
553                                 break;
554                 }
555                 change_list->insert(it, TocItem(dit, 1, str, output_active));
556         }
557 }
558
559
560 void Changes::updateBuffer(Buffer const & buf)
561 {
562         bool const changed = isChanged();
563         buf.setChangesPresent(buf.areChangesPresent() || changed);
564         previously_changed_ = changed;
565 }
566
567
568 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
569                       double const x2, FontInfo const & font) const
570 {
571         if (!changed())
572                 return;
573         // Calculate 1/3 height of font
574         FontMetrics const & fm = theFontMetrics(font);
575         double const y_bar = deleted() ? y - fm.maxAscent() / 3
576                 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
577         pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
578                      Painter::line_solid, pi.base.solidLineThickness());
579 }
580
581
582 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
583                       double const x2, double const y2) const
584 {
585         /*
586          * y1      /
587          *        /
588          *       /
589          *      /
590          *     /
591          * y2 /_____
592          *    x1  x2
593          */
594         switch(type) {
595         case UNCHANGED:
596                 return;
597         case INSERTED:
598                 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
599                              color(), Painter::line_solid,
600                              pi.base.solidLineThickness());
601                 return;
602         case DELETED:
603                 // FIXME: we cannot use antialias since we keep drawing on the same
604                 // background with the current painting mechanism.
605                 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
606                              color(), Painter::line_solid_aliased,
607                              pi.base.solidLineThickness());
608                 return;
609         }
610 }
611
612
613 } // namespace lyx