]> git.lyx.org Git - lyx.git/blob - src/Changes.cpp
Fix screen display of parts and chapters in default classes
[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 "LyXRC.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_initials);
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         if (oldChange.type != Change::UNCHANGED) {
432                 if (oldChange.type != Change::DELETED || runparams.ctObject != OutputParams::CT_OMITOBJECT) {
433                         // close \lyxadded or \lyxdeleted
434                         os << '}';
435                         column++;
436                 }
437                 if (oldChange.type == Change::DELETED
438                     && !runparams.wasDisplayMath)
439                         --runparams.inulemcmd;
440         }
441
442         docstring chgTime;
443         chgTime += asctime(gmtime(&change.changetime));
444         // remove trailing '\n'
445         chgTime.erase(chgTime.end() - 1);
446
447         docstring macro_beg;
448         if (change.type == Change::DELETED) {
449                 if (runparams.ctObject == OutputParams::CT_OMITOBJECT)
450                         return 0;
451                 else if (runparams.ctObject == OutputParams::CT_OBJECT)
452                         macro_beg = from_ascii("\\lyxobjdeleted");
453                 else if (runparams.ctObject == OutputParams::CT_DISPLAYOBJECT)
454                         macro_beg = from_ascii("\\lyxdisplayobjdeleted");
455                 else if (runparams.ctObject == OutputParams::CT_UDISPLAYOBJECT)
456                         macro_beg = from_ascii("\\lyxudisplayobjdeleted");
457                 else {
458                         macro_beg = from_ascii("\\lyxdeleted");
459                         if (!runparams.inDisplayMath)
460                                 ++runparams.inulemcmd;
461                 }
462         }
463         else if (change.type == Change::INSERTED)
464                 macro_beg = from_ascii("\\lyxadded");
465
466         docstring str = getLaTeXMarkup(macro_beg,
467                                        bparams.authors().get(change.author),
468                                        chgTime, runparams);
469
470         os << str;
471         column += str.size();
472
473         return column;
474 }
475
476
477 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
478                             Change const & old, Change const & change)
479 {
480         if (old == change)
481                 return;
482
483         column = 0;
484
485         int const buffer_id = bparams.authors().get(change.author).bufferId();
486
487         switch (change.type) {
488                 case Change::UNCHANGED:
489                         os << "\n\\change_unchanged\n";
490                         break;
491
492                 case Change::DELETED:
493                         os << "\n\\change_deleted " << buffer_id
494                                 << " " << change.changetime << "\n";
495                         break;
496
497                 case Change::INSERTED:
498                         os << "\n\\change_inserted " << buffer_id
499                                 << " " << change.changetime << "\n";
500                         break;
501         }
502 }
503
504
505 void Changes::checkAuthors(AuthorList const & authorList) const
506 {
507         for (ChangeRange const & cr : table_)
508                 if (cr.change.type != Change::UNCHANGED)
509                         authorList.get(cr.change.author).setUsed(true);
510 }
511
512
513 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
514                        bool output_active, TocBackend & backend) const
515 {
516         if (table_.empty())
517                 return;
518
519         shared_ptr<Toc> change_list = backend.toc("change");
520         AuthorList const & author_list = buffer.params().authors();
521         DocIterator dit = cdit;
522
523         for (ChangeRange const & cr : table_) {
524                 docstring str;
525                 switch (cr.change.type) {
526                 case Change::UNCHANGED:
527                         continue;
528                 case Change::DELETED:
529                         // ✂ U+2702 BLACK SCISSORS
530                         str.push_back(0x2702);
531                         break;
532                 case Change::INSERTED:
533                         // ✍ U+270D WRITING HAND
534                         str.push_back(0x270d);
535                         break;
536                 }
537                 dit.pos() = cr.range.start;
538                 Paragraph const & par = dit.paragraph();
539                 str += " " + par.asString(cr.range.start, min(par.size(), cr.range.end));
540                 if (cr.range.end > par.size())
541                         // ¶ U+00B6 PILCROW SIGN
542                         str.push_back(0xb6);
543                 docstring const & author = author_list.get(cr.change.author).name();
544                 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
545                 if (it == change_list->end()) {
546                         change_list->push_back(TocItem(dit, 0, author, true));
547                         change_list->push_back(TocItem(dit, 1, str, output_active));
548                         continue;
549                 }
550                 for (++it; it != change_list->end(); ++it) {
551                         if (it->depth() == 0 && it->str() != author)
552                                 break;
553                 }
554                 change_list->insert(it, TocItem(dit, 1, str, output_active));
555         }
556 }
557
558
559 void Change::paintCue(PainterInfo & pi, double const x1, double const y,
560                       double const x2, FontInfo const & font) const
561 {
562         if (!changed() || (!lyxrc.ct_additions_underlined && inserted()))
563                 return;
564         // Calculate 1/3 height of font
565         FontMetrics const & fm = theFontMetrics(font);
566         double const y_bar = deleted() ? y - fm.maxAscent() / 3
567                 : y + 2 * pi.base.solidLineOffset() + pi.base.solidLineThickness();
568         pi.pain.line(int(x1), int(y_bar), int(x2), int(y_bar), color(),
569                      Painter::line_solid, pi.base.solidLineThickness());
570 }
571
572
573 void Change::paintCue(PainterInfo & pi, double const x1, double const y1,
574                       double const x2, double const y2) const
575 {
576         /*
577          * y1      /
578          *        /
579          *       /
580          *      /
581          *     /
582          * y2 /_____
583          *    x1  x2
584          */
585         switch(type) {
586         case UNCHANGED:
587                 return;
588         case INSERTED: {
589                 if (!lyxrc.ct_additions_underlined)
590                         return;
591                 pi.pain.line(int(x1), int(y2) + 1, int(x2), int(y2) + 1,
592                              color(), Painter::line_solid,
593                              pi.base.solidLineThickness());
594                 return;
595         }
596         case DELETED:
597                 // FIXME: we cannot use antialias since we keep drawing on the same
598                 // background with the current painting mechanism.
599                 pi.pain.line(int(x1), int(y2), int(x2), int(y1),
600                              color(), Painter::line_solid_aliased,
601                              pi.base.solidLineThickness());
602                 return;
603         }
604 }
605
606
607 } // namespace lyx