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