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