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