]> git.lyx.org Git - lyx.git/blob - src/Changes.cpp
Warn in GUI when mixing Title and InTitle layouts
[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 "OutputParams.h"
23 #include "Paragraph.h"
24 #include "TocBackend.h"
25
26 #include "support/debug.h"
27 #include "support/gettext.h"
28 #include "support/lassert.h"
29 #include "support/lstrings.h"
30 #include "support/mutex.h"
31
32 #include "frontends/alert.h"
33
34 #include <ostream>
35
36 using namespace std;
37
38 namespace lyx {
39
40 /*
41  * Class Change has a changetime field that specifies the exact time at which
42  * a specific change was made. The change time is used as a guidance for the
43  * user while editing his document. Presently, it is not considered for LaTeX
44  * export.
45  * When merging two adjacent changes, the changetime is not considered,
46  * only the equality of the change type and author is checked (in method
47  * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
48  * the later change time is preserved.
49  */
50
51 bool Change::isSimilarTo(Change const & change) const
52 {
53         if (type != change.type)
54                 return false;
55
56         if (type == Change::UNCHANGED)
57                 return true;
58
59         return author == change.author;
60 }
61
62
63 Color Change::color() const
64 {
65         Color color = Color_none;
66         switch (author % 5) {
67                 case 0:
68                         color = Color_changedtextauthor1;
69                         break;
70                 case 1:
71                         color = Color_changedtextauthor2;
72                         break;
73                 case 2:
74                         color = Color_changedtextauthor3;
75                         break;
76                 case 3:
77                         color = Color_changedtextauthor4;
78                         break;
79                 case 4:
80                         color = Color_changedtextauthor5;
81                         break;
82         }
83
84         if (deleted())
85                 color.mergeColor = Color_deletedtextmodifier;
86
87         return color;
88 }
89
90
91 bool operator==(Change const & l, Change const & r)
92 {
93         if (l.type != r.type)
94                 return false;
95
96         // two changes of type UNCHANGED are always equal
97         if (l.type == Change::UNCHANGED)
98                 return true;
99
100         return l.author == r.author && l.changetime == r.changetime;
101 }
102
103
104 bool operator!=(Change const & l, Change const & r)
105 {
106         return !(l == r);
107 }
108
109
110 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
111 {
112         return r1.start == r2.start && r1.end == r2.end;
113 }
114
115
116 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
117 {
118         return !(r1 == r2);
119 }
120
121
122 bool Changes::Range::intersects(Range const & r) const
123 {
124         return r.start < end && r.end > start; // end itself is not in the range!
125 }
126
127
128 void Changes::set(Change const & change, pos_type const pos)
129 {
130         set(change, pos, pos + 1);
131 }
132
133
134 void Changes::set(Change const & change, pos_type const start, pos_type const end)
135 {
136         if (change.type != Change::UNCHANGED) {
137                 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
138                         << ", author: " << change.author 
139                         << ", time: " << long(change.changetime)
140                         << ") in range (" << start << ", " << end << ")");
141                 if (!isChanged())
142                         is_update_required_ = true;
143         }
144
145         Range const newRange(start, end);
146
147         ChangeTable::iterator it = table_.begin();
148
149         for (; it != table_.end(); ) {
150                 // current change starts like or follows new change
151                 if (it->range.start >= start) {
152                         break;
153                 }
154
155                 // new change intersects with existing change
156                 if (it->range.end > start) {
157                         pos_type oldEnd = it->range.end;
158                         it->range.end = start;
159
160                         LYXERR(Debug::CHANGES, "  cutting tail of type " << it->change.type
161                                 << " resulting in range (" << it->range.start << ", "
162                                 << it->range.end << ")");
163
164                         ++it;
165                         if (oldEnd >= end) {
166                                 LYXERR(Debug::CHANGES, "  inserting tail in range ("
167                                         << end << ", " << oldEnd << ")");
168                                 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
169                         }
170                         continue;
171                 }
172
173                 ++it;
174         }
175
176         if (change.type != Change::UNCHANGED) {
177                 LYXERR(Debug::CHANGES, "  inserting change");
178                 it = table_.insert(it, ChangeRange(change, Range(start, end)));
179                 ++it;
180         }
181
182         for (; it != table_.end(); ) {
183                 // new change 'contains' existing change
184                 if (newRange.contains(it->range)) {
185                         LYXERR(Debug::CHANGES, "  removing subrange ("
186                                 << it->range.start << ", " << it->range.end << ")");
187                         it = table_.erase(it);
188                         continue;
189                 }
190
191                 // new change precedes existing change
192                 if (it->range.start >= end)
193                         break;
194
195                 // new change intersects with existing change
196                 it->range.start = end;
197                 LYXERR(Debug::CHANGES, "  cutting head of type "
198                         << it->change.type << " resulting in range ("
199                         << end << ", " << it->range.end << ")");
200                 break; // no need for another iteration
201         }
202
203         merge();
204 }
205
206
207 void Changes::erase(pos_type const pos)
208 {
209         LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
210
211         ChangeTable::iterator it = table_.begin();
212         ChangeTable::iterator end = table_.end();
213
214         for (; it != end; ++it) {
215                 // range (pos,pos+x) becomes (pos,pos+x-1)
216                 if (it->range.start > pos)
217                         --(it->range.start);
218                 // range (pos-x,pos) stays (pos-x,pos)
219                 if (it->range.end > pos)
220                         --(it->range.end);
221         }
222
223         merge();
224 }
225
226
227 void Changes::insert(Change const & change, lyx::pos_type pos)
228 {
229         if (change.type != Change::UNCHANGED) {
230                 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
231                         << " at position " << pos);
232         }
233
234         ChangeTable::iterator it = table_.begin();
235         ChangeTable::iterator end = table_.end();
236
237         for (; it != end; ++it) {
238                 // range (pos,pos+x) becomes (pos+1,pos+x+1)
239                 if (it->range.start >= pos)
240                         ++(it->range.start);
241
242                 // range (pos-x,pos) stays as it is
243                 if (it->range.end > pos)
244                         ++(it->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
255         ChangeTable::const_iterator it = table_.begin();
256         ChangeTable::const_iterator const end = table_.end();
257
258         for (; it != end; ++it) {
259                 if (it->range.contains(pos))
260                         return it->change;
261         }
262
263         return noChange;
264 }
265
266
267 bool Changes::isDeleted(pos_type start, pos_type end) const
268 {
269         ChangeTable::const_iterator it = table_.begin();
270         ChangeTable::const_iterator const itend = table_.end();
271
272         for (; it != itend; ++it) {
273                 if (it->range.contains(Range(start, end))) {
274                         LYXERR(Debug::CHANGES, "range ("
275                                 << start << ", " << end << ") fully contains ("
276                                 << it->range.start << ", " << it->range.end
277                                 << ") of type " << it->change.type);
278                         return it->change.type == Change::DELETED;
279                 }
280         }
281         return false;
282 }
283
284
285 bool Changes::isChanged(pos_type const start, pos_type const end) const
286 {
287         ChangeTable::const_iterator it = table_.begin();
288         ChangeTable::const_iterator const itend = table_.end();
289
290         for (; it != itend; ++it) {
291                 if (it->range.intersects(Range(start, end))) {
292                         LYXERR(Debug::CHANGES, "found intersection of range ("
293                                 << start << ", " << end << ") with ("
294                                 << it->range.start << ", " << it->range.end
295                                 << ") of type " << it->change.type);
296                         return true;
297                 }
298         }
299         return false;
300 }
301
302
303 bool Changes::isChanged() const
304 {
305         ChangeTable::const_iterator it = table_.begin();
306         ChangeTable::const_iterator const itend = table_.end();
307         for (; it != itend; ++it) {
308                 if (it->change.changed())
309                         return true;
310         }
311         return false;
312 }
313
314
315 void Changes::merge()
316 {
317         bool merged = false;
318         ChangeTable::iterator it = table_.begin();
319
320         while (it != table_.end()) {
321                 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
322                         << " and range (" << it->range.start << ", " << it->range.end
323                         << ")");
324
325                 if (it->range.start == it->range.end) {
326                         LYXERR(Debug::CHANGES, "removing empty range for pos "
327                                 << it->range.start);
328
329                         table_.erase(it);
330                         merged = true;
331                         // start again
332                         it = table_.begin();
333                         continue;
334                 }
335
336                 if (it + 1 == table_.end())
337                         break;
338
339                 if (it->change.isSimilarTo((it + 1)->change)
340                     && it->range.end == (it + 1)->range.start) {
341                         LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
342                                 << it->range.end << ") and (" << (it + 1)->range.start << ", "
343                                 << (it + 1)->range.end << ")");
344
345                         (it + 1)->range.start = it->range.start;
346                         (it + 1)->change.changetime = max(it->change.changetime,
347                                                           (it + 1)->change.changetime);
348                         table_.erase(it);
349                         merged = true;
350                         // start again
351                         it = table_.begin();
352                         continue;
353                 }
354
355                 ++it;
356         }
357         if (merged && !isChanged())
358                 is_update_required_ = true;
359 }
360
361
362 namespace {
363
364 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
365                          docstring const & chgTime,
366                          OutputParams const & runparams)
367 {
368         if (macro.empty())
369                 return docstring();
370
371         docstring uncodable_author;
372         odocstringstream ods;
373
374         ods << macro;
375         // convert utf8 author name to something representable
376         // in the current encoding
377         pair<docstring, docstring> author_latexed =
378                 runparams.encoding->latexString(author, runparams.dryrun);
379         if (!author_latexed.second.empty()) {
380                 LYXERR0("Omitting uncodable characters '"
381                         << author_latexed.second
382                         << "' in change author name!");
383                 uncodable_author = author;
384         }
385         ods << author_latexed.first << "}{" << chgTime << "}{";
386
387         // warn user (once) if we found uncodable glyphs.
388         if (!uncodable_author.empty()) {
389                 static std::set<docstring> warned_authors;
390                 static Mutex warned_mutex;
391                 Mutex::Locker locker(&warned_mutex);
392                 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
393                         frontend::Alert::warning(_("Uncodable character in author name"),
394                                 support::bformat(_("The author name '%1$s',\n"
395                                   "used for change tracking, contains the following glyphs that\n"
396                                   "cannot be represented in the current encoding: %2$s.\n"
397                                   "These glyphs will be omitted in the exported LaTeX file.\n\n"
398                                   "Choose an appropriate document encoding (such as utf8)\n"
399                                   "or change the spelling of the author name."),
400                                 uncodable_author, author_latexed.second));
401                         warned_authors.insert(uncodable_author);
402                 }
403         }
404
405         return ods.str();
406 }
407
408 } //namespace anon
409
410
411 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
412                              Change const & oldChange, Change const & change,
413                              OutputParams const & runparams)
414 {
415         if (!bparams.output_changes || oldChange == change)
416                 return 0;
417
418         int column = 0;
419
420         if (oldChange.type != Change::UNCHANGED) {
421                 // close \lyxadded or \lyxdeleted
422                 os << '}';
423                 column++;
424                 if (oldChange.type == Change::DELETED)
425                         --runparams.inulemcmd;
426         }
427
428         docstring chgTime;
429         chgTime += asctime(gmtime(&change.changetime));
430         // remove trailing '\n'
431         chgTime.erase(chgTime.end() - 1);
432
433         docstring macro_beg;
434         if (change.type == Change::DELETED) {
435                 macro_beg = from_ascii("\\lyxdeleted{");
436                 ++runparams.inulemcmd;
437         }
438         else if (change.type == Change::INSERTED)
439                 macro_beg = from_ascii("\\lyxadded{");
440         
441         docstring str = getLaTeXMarkup(macro_beg,
442                                        bparams.authors().get(change.author).name(),
443                                        chgTime, runparams);
444         
445         os << str;
446         column += str.size();
447
448         return column;
449 }
450
451
452 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
453                             Change const & old, Change const & change)
454 {
455         if (old == change)
456                 return;
457
458         column = 0;
459
460         int const buffer_id = bparams.authors().get(change.author).bufferId();
461
462         switch (change.type) {
463                 case Change::UNCHANGED:
464                         os << "\n\\change_unchanged\n";
465                         break;
466
467                 case Change::DELETED:
468                         os << "\n\\change_deleted " << buffer_id
469                                 << " " << change.changetime << "\n";
470                         break;
471
472                 case Change::INSERTED:
473                         os << "\n\\change_inserted " << buffer_id
474                                 << " " << change.changetime << "\n";
475                         break;
476         }
477 }
478
479
480 void Changes::checkAuthors(AuthorList const & authorList)
481 {
482         ChangeTable::const_iterator it = table_.begin();
483         ChangeTable::const_iterator endit = table_.end();
484         for ( ; it != endit ; ++it) 
485                 if (it->change.type != Change::UNCHANGED)
486                         authorList.get(it->change.author).setUsed(true);
487 }
488
489
490 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
491         bool output_active) const
492 {
493         if (table_.empty())
494                 return;
495
496         shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
497         AuthorList const & author_list = buffer.params().authors();
498         DocIterator dit = cdit;
499
500         ChangeTable::const_iterator it = table_.begin();
501         ChangeTable::const_iterator const itend = table_.end();
502         for (; it != itend; ++it) {
503                 docstring str;
504                 switch (it->change.type) {
505                 case Change::UNCHANGED:
506                         continue;
507                 case Change::DELETED:
508                         // ✂ U+2702 BLACK SCISSORS
509                         str.push_back(0x2702);
510                         break;
511                 case Change::INSERTED:
512                         // ✍ U+270D WRITING HAND
513                         str.push_back(0x270d);
514                         break;
515                 }
516                 dit.pos() = it->range.start;
517                 Paragraph const & par = dit.paragraph();
518                 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
519                 if (it->range.end > par.size())
520                         // ¶ U+00B6 PILCROW SIGN
521                         str.push_back(0xb6);
522                 docstring const & author = author_list.get(it->change.author).name();
523                 Toc::iterator it = change_list->item(0, author);
524                 if (it == change_list->end()) {
525                         change_list->push_back(TocItem(dit, 0, author, true));
526                         change_list->push_back(TocItem(dit, 1, str, output_active,
527                                 support::wrapParas(str, 4)));
528                         continue;
529                 }
530                 for (++it; it != change_list->end(); ++it) {
531                         if (it->depth() == 0 && it->str() != author)
532                                 break;
533                 }
534                 change_list->insert(it, TocItem(dit, 1, str, output_active,
535                         support::wrapParas(str, 4)));
536         }
537 }
538
539
540 void Changes::updateBuffer(Buffer const & buf)
541 {
542         is_update_required_ = false;
543         if (!buf.areChangesPresent() && isChanged())
544                 buf.setChangesPresent(true);
545 }
546
547
548
549
550 } // namespace lyx