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