]> git.lyx.org Git - lyx.git/blob - src/Text2.cpp
Use a more transparent lexic for bullet label functions
[lyx.git] / src / Text2.cpp
1 /**
2  * \file text2.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Asger Alstrup
7  * \author Lars Gullik Bjønnes
8  * \author Alfredo Braunstein
9  * \author Jean-Marc Lasgouttes
10  * \author Angus Leeming
11  * \author John Levon
12  * \author André Pönitz
13  * \author Allan Rae
14  * \author Stefan Schimanski
15  * \author Dekel Tsur
16  * \author Jürgen Vigna
17  *
18  * Full author contact details are available in file CREDITS.
19  */
20
21 #include <config.h>
22
23 #include "Text.h"
24
25 #include "Buffer.h"
26 #include "BufferParams.h"
27 #include "BufferView.h"
28 #include "Changes.h"
29 #include "Cursor.h"
30 #include "Language.h"
31 #include "Layout.h"
32 #include "LyXRC.h"
33 #include "Paragraph.h"
34 #include "ParagraphParameters.h"
35 #include "TextClass.h"
36 #include "TextMetrics.h"
37
38 #include "insets/InsetText.h"
39
40 #include "support/lassert.h"
41 #include "support/gettext.h"
42
43 #include <sstream>
44
45 using namespace std;
46
47 namespace lyx {
48
49 bool Text::isMainText() const
50 {
51         return &owner_->buffer().text() == this;
52 }
53
54
55 // Note that this is supposed to return a fully realized font.
56 FontInfo Text::layoutFont(pit_type const pit) const
57 {
58         Layout const & layout = pars_[pit].layout();
59
60         if (!pars_[pit].getDepth())  {
61                 FontInfo lf = layout.resfont;
62                 // In case the default family has been customized
63                 if (layout.font.family() == INHERIT_FAMILY)
64                         lf.setFamily(owner_->buffer().params().getFont().fontInfo().family());
65                 FontInfo icf = owner_->getLayout().font();
66                 icf.realize(lf);
67                 return icf;
68         }
69
70         FontInfo font = layout.font;
71         // Realize with the fonts of lesser depth.
72         //font.realize(outerFont(pit));
73         font.realize(owner_->buffer().params().getFont().fontInfo());
74
75         return font;
76 }
77
78
79 // Note that this is supposed to return a fully realized font.
80 FontInfo Text::labelFont(Paragraph const & par) const
81 {
82         Buffer const & buffer = owner_->buffer();
83         Layout const & layout = par.layout();
84
85         if (!par.getDepth()) {
86                 FontInfo lf = layout.reslabelfont;
87                 // In case the default family has been customized
88                 if (layout.labelfont.family() == INHERIT_FAMILY)
89                         lf.setFamily(buffer.params().getFont().fontInfo().family());
90                 return lf;
91         }
92
93         FontInfo font = layout.labelfont;
94         // Realize with the fonts of lesser depth.
95         font.realize(buffer.params().getFont().fontInfo());
96
97         return font;
98 }
99
100
101 void Text::setCharFont(pit_type pit,
102                 pos_type pos, Font const & fnt, Font const & display_font)
103 {
104         Buffer const & buffer = owner_->buffer();
105         Font font = fnt;
106         Layout const & layout = pars_[pit].layout();
107
108         // Get concrete layout font to reduce against
109         FontInfo layoutfont;
110
111         if (pos < pars_[pit].beginOfBody())
112                 layoutfont = layout.labelfont;
113         else
114                 layoutfont = layout.font;
115
116         // Realize against environment font information
117         if (pars_[pit].getDepth()) {
118                 pit_type tp = pit;
119                 while (!layoutfont.resolved() &&
120                        tp != pit_type(paragraphs().size()) &&
121                        pars_[tp].getDepth()) {
122                         tp = outerHook(tp);
123                         if (tp != pit_type(paragraphs().size()))
124                                 layoutfont.realize(pars_[tp].layout().font);
125                 }
126         }
127
128         // Inside inset, apply the inset's font attributes if any
129         // (charstyle!)
130         if (!isMainText())
131                 layoutfont.realize(display_font.fontInfo());
132
133         layoutfont.realize(buffer.params().getFont().fontInfo());
134
135         // Now, reduce font against full layout font
136         font.fontInfo().reduce(layoutfont);
137
138         pars_[pit].setFont(pos, font);
139 }
140
141
142 void Text::setInsetFont(BufferView const & bv, pit_type pit,
143                 pos_type pos, Font const & font)
144 {
145         Inset * const inset = pars_[pit].getInset(pos);
146         LASSERT(inset && inset->resetFontEdit(), return);
147
148         idx_type endidx = inset->nargs();
149         for (CursorSlice cs(*inset); cs.idx() != endidx; ++cs.idx()) {
150                 Text * text = cs.text();
151                 if (text) {
152                         // last position of the cell
153                         CursorSlice cellend = cs;
154                         cellend.pit() = cellend.lastpit();
155                         cellend.pos() = cellend.lastpos();
156                         text->setFont(bv, cs, cellend, font);
157                 }
158         }
159 }
160
161
162 void Text::setLayout(pit_type start, pit_type end,
163                      docstring const & layout)
164 {
165         LASSERT(start != end, return);
166
167         Buffer const & buffer = owner_->buffer();
168         BufferParams const & bp = buffer.params();
169         Layout const & lyxlayout = bp.documentClass()[layout];
170
171         for (pit_type pit = start; pit != end; ++pit) {
172                 Paragraph & par = pars_[pit];
173                 // Is this a separating paragraph? If so,
174                 // this needs to be standard layout
175                 bool const is_separator = par.size() == 1
176                                 && par.isEnvSeparator(0);
177                 par.applyLayout(is_separator ? bp.documentClass().defaultLayout() : lyxlayout);
178                 if (lyxlayout.margintype == MARGIN_MANUAL)
179                         par.setLabelWidthString(par.expandLabel(lyxlayout, bp));
180         }
181
182         deleteEmptyParagraphMechanism(start, end - 1, bp.track_changes);
183 }
184
185
186 // set layout over selection and make a total rebreak of those paragraphs
187 void Text::setLayout(Cursor & cur, docstring const & layout)
188 {
189         LBUFERR(this == cur.text());
190
191         pit_type start = cur.selBegin().pit();
192         pit_type end = cur.selEnd().pit() + 1;
193         cur.recordUndoSelection();
194         setLayout(start, end, layout);
195         cur.fixIfBroken();
196         cur.setCurrentFont();
197         cur.forceBufferUpdate();
198 }
199
200
201 static bool changeDepthAllowed(Text::DEPTH_CHANGE type,
202                         Paragraph const & par, int max_depth)
203 {
204         int const depth = par.params().depth();
205         if (type == Text::INC_DEPTH && depth < max_depth)
206                 return true;
207         if (type == Text::DEC_DEPTH && depth > 0)
208                 return true;
209         return false;
210 }
211
212
213 bool Text::changeDepthAllowed(Cursor const & cur, DEPTH_CHANGE type) const
214 {
215         LBUFERR(this == cur.text());
216         // this happens when selecting several cells in tabular (bug 2630)
217         if (cur.selBegin().idx() != cur.selEnd().idx())
218                 return false;
219
220         pit_type const beg = cur.selBegin().pit();
221         pit_type const end = cur.selEnd().pit() + 1;
222         int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
223
224         for (pit_type pit = beg; pit != end; ++pit) {
225                 if (lyx::changeDepthAllowed(type, pars_[pit], max_depth))
226                         return true;
227                 max_depth = pars_[pit].getMaxDepthAfter();
228         }
229         return false;
230 }
231
232
233 void Text::changeDepth(Cursor const & cur, DEPTH_CHANGE type)
234 {
235         LBUFERR(this == cur.text());
236         pit_type const beg = cur.selBegin().pit();
237         pit_type const end = cur.selEnd().pit() + 1;
238         cur.recordUndoSelection();
239         int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
240
241         for (pit_type pit = beg; pit != end; ++pit) {
242                 Paragraph & par = pars_[pit];
243                 if (lyx::changeDepthAllowed(type, par, max_depth)) {
244                         int const depth = par.params().depth();
245                         if (type == INC_DEPTH)
246                                 par.params().depth(depth + 1);
247                         else
248                                 par.params().depth(depth - 1);
249                 }
250                 max_depth = par.getMaxDepthAfter();
251         }
252         // this handles the counter labels, and also fixes up
253         // depth values for follow-on (child) paragraphs
254         cur.forceBufferUpdate();
255 }
256
257
258 void Text::setFont(Cursor & cur, Font const & font, bool toggleall)
259 {
260         LASSERT(this == cur.text(), return);
261
262         // If there is a selection, record undo before the cursor font is changed.
263         if (cur.selection())
264                 cur.recordUndoSelection();
265
266         // Set the current_font
267         // Determine basis font
268         FontInfo layoutfont;
269         pit_type pit = cur.pit();
270         if (cur.pos() < pars_[pit].beginOfBody())
271                 layoutfont = labelFont(pars_[pit]);
272         else
273                 layoutfont = layoutFont(pit);
274
275         // Update current font
276         cur.real_current_font.update(font,
277                                         cur.buffer()->params().language,
278                                         toggleall);
279
280         // Reduce to implicit settings
281         cur.current_font = cur.real_current_font;
282         cur.current_font.fontInfo().reduce(layoutfont);
283         // And resolve it completely
284         cur.real_current_font.fontInfo().realize(layoutfont);
285
286         // if there is no selection that's all we need to do
287         if (!cur.selection())
288                 return;
289
290         // Ok, we have a selection.
291         Font newfont = font;
292
293         if (toggleall) {
294                 // Toggling behaves as follows: We check the first character of the
295                 // selection. If it's (say) got EMPH on, then we set to off; if off,
296                 // then to on. With families and the like, we set it to INHERIT, if
297                 // we already have it.
298                 CursorSlice const & sl = cur.selBegin();
299                 Text const & text = *sl.text();
300                 Paragraph const & par = text.getPar(sl.pit());
301
302                 // get font at the position
303                 Font oldfont = par.getFont(cur.bv().buffer().params(), sl.pos(),
304                         text.outerFont(sl.pit()));
305                 FontInfo const & oldfi = oldfont.fontInfo();
306
307                 FontInfo & newfi = newfont.fontInfo();
308
309                 FontFamily newfam = newfi.family();
310                 if (newfam !=   INHERIT_FAMILY && newfam != IGNORE_FAMILY &&
311                                 newfam == oldfi.family())
312                         newfi.setFamily(INHERIT_FAMILY);
313
314                 FontSeries newser = newfi.series();
315                 if (newser == BOLD_SERIES && oldfi.series() == BOLD_SERIES)
316                         newfi.setSeries(INHERIT_SERIES);
317
318                 FontShape newshp = newfi.shape();
319                 if (newshp != INHERIT_SHAPE && newshp != IGNORE_SHAPE &&
320                                 newshp == oldfi.shape())
321                         newfi.setShape(INHERIT_SHAPE);
322
323                 ColorCode newcol = newfi.color();
324                 if (newcol != Color_none && newcol != Color_inherit
325                     && newcol != Color_ignore && newcol == oldfi.color())
326                         newfi.setColor(Color_none);
327
328                 // ON/OFF ones
329                 if (newfi.emph() == FONT_TOGGLE)
330                         newfi.setEmph(oldfi.emph() == FONT_OFF ? FONT_ON : FONT_OFF);
331                 if (newfi.underbar() == FONT_TOGGLE)
332                         newfi.setUnderbar(oldfi.underbar() == FONT_OFF ? FONT_ON : FONT_OFF);
333                 if (newfi.strikeout() == FONT_TOGGLE)
334                         newfi.setStrikeout(oldfi.strikeout() == FONT_OFF ? FONT_ON : FONT_OFF);
335                 if (newfi.xout() == FONT_TOGGLE)
336                         newfi.setXout(oldfi.xout() == FONT_OFF ? FONT_ON : FONT_OFF);
337                 if (newfi.uuline() == FONT_TOGGLE)
338                         newfi.setUuline(oldfi.uuline() == FONT_OFF ? FONT_ON : FONT_OFF);
339                 if (newfi.uwave() == FONT_TOGGLE)
340                         newfi.setUwave(oldfi.uwave() == FONT_OFF ? FONT_ON : FONT_OFF);
341                 if (newfi.noun() == FONT_TOGGLE)
342                         newfi.setNoun(oldfi.noun() == FONT_OFF ? FONT_ON : FONT_OFF);
343                 if (newfi.number() == FONT_TOGGLE)
344                         newfi.setNumber(oldfi.number() == FONT_OFF ? FONT_ON : FONT_OFF);
345                 if (newfi.nospellcheck() == FONT_TOGGLE)
346                         newfi.setNoSpellcheck(oldfi.nospellcheck() == FONT_OFF ? FONT_ON : FONT_OFF);
347         }
348
349         setFont(cur.bv(), cur.selectionBegin().top(),
350                 cur.selectionEnd().top(), newfont);
351 }
352
353
354 void Text::setFont(BufferView const & bv, CursorSlice const & begin,
355                 CursorSlice const & end, Font const & font)
356 {
357         Buffer const & buffer = bv.buffer();
358
359         // Don't use forwardChar here as ditend might have
360         // pos() == lastpos() and forwardChar would miss it.
361         // Can't use forwardPos either as this descends into
362         // nested insets.
363         Language const * language = buffer.params().language;
364         for (CursorSlice dit = begin; dit != end; dit.forwardPos()) {
365                 if (dit.pos() == dit.lastpos())
366                         continue;
367                 pit_type const pit = dit.pit();
368                 pos_type const pos = dit.pos();
369                 Inset * inset = pars_[pit].getInset(pos);
370                 if (inset && inset->resetFontEdit()) {
371                         // We need to propagate the font change to all
372                         // text cells of the inset (bugs 1973, 6919).
373                         setInsetFont(bv, pit, pos, font);
374                 }
375                 TextMetrics const & tm = bv.textMetrics(this);
376                 Font f = tm.displayFont(pit, pos);
377                 f.update(font, language);
378                 setCharFont(pit, pos, f, tm.font_);
379                 // font change may change language...
380                 // spell checker has to know that
381                 pars_[pit].requestSpellCheck(pos);
382         }
383 }
384
385
386 bool Text::cursorTop(Cursor & cur)
387 {
388         LBUFERR(this == cur.text());
389         return setCursor(cur, 0, 0);
390 }
391
392
393 bool Text::cursorBottom(Cursor & cur)
394 {
395         LBUFERR(this == cur.text());
396         return setCursor(cur, cur.lastpit(), prev(paragraphs().end(), 1)->size());
397 }
398
399
400 void Text::toggleFree(Cursor & cur, Font const & font, bool toggleall)
401 {
402         LBUFERR(this == cur.text());
403         // If the mask is completely neutral, tell user
404         if (font.fontInfo() == ignore_font && font.language() == ignore_language) {
405                 // Could only happen with user style
406                 cur.message(_("No font change defined."));
407                 return;
408         }
409
410         // Try implicit word selection
411         // If there is a change in the language the implicit word selection
412         // is disabled.
413         CursorSlice const resetCursor = cur.top();
414         bool const implicitSelection =
415                 font.language() == ignore_language
416                 && font.fontInfo().number() == FONT_IGNORE
417                 && selectWordWhenUnderCursor(cur, WHOLE_WORD_STRICT);
418
419         // Set font
420         setFont(cur, font, toggleall);
421
422         // Implicit selections are cleared afterwards
423         // and cursor is set to the original position.
424         if (implicitSelection) {
425                 cur.clearSelection();
426                 cur.top() = resetCursor;
427                 cur.resetAnchor();
428                 cur.setCurrentFont();
429         }
430 }
431
432
433 docstring Text::getStringForDialog(Cursor & cur)
434 {
435         LBUFERR(this == cur.text());
436
437         if (cur.selection())
438                 return cur.selectionAsString(false);
439
440         // Try implicit word selection. If there is a change
441         // in the language the implicit word selection is
442         // disabled.
443         selectWordWhenUnderCursor(cur, WHOLE_WORD);
444         docstring const & retval = cur.selectionAsString(false);
445         cur.clearSelection();
446         return retval;
447 }
448
449
450 void Text::setLabelWidthStringToSequence(Cursor const & cur,
451                 docstring const & s)
452 {
453         Cursor c = cur;
454         // Find first of same layout in sequence
455         while (!isFirstInSequence(c.pit())) {
456                 c.pit() = depthHook(c.pit(), c.paragraph().getDepth());
457         }
458
459         // now apply label width string to every par
460         // in sequence
461         depth_type const depth = c.paragraph().getDepth();
462         Layout const & layout = c.paragraph().layout();
463         for ( ; c.pit() <= c.lastpit() ; ++c.pit()) {
464                 while (c.paragraph().getDepth() > depth) {
465                         ++c.pit();
466                         if (c.pit() > c.lastpit())
467                                 return;
468                 }
469                 if (c.paragraph().getDepth() < depth)
470                         return;
471                 if (c.paragraph().layout() != layout)
472                         return;
473                 c.recordUndo();
474                 c.paragraph().setLabelWidthString(s);
475         }
476 }
477
478
479 void Text::setParagraphs(Cursor const & cur, docstring const & arg, bool merge)
480 {
481         LBUFERR(cur.text());
482
483         //FIXME UNICODE
484         string const argument = to_utf8(arg);
485         depth_type priordepth = -1;
486         Layout priorlayout;
487         Cursor c(cur.bv());
488         c.setCursor(cur.selectionBegin());
489         for ( ; c <= cur.selectionEnd() ; ++c.pit()) {
490                 Paragraph & par = c.paragraph();
491                 ParagraphParameters params = par.params();
492                 params.read(argument, merge);
493                 // Changes to label width string apply to all paragraphs
494                 // with same layout in a sequence.
495                 // Do this only once for a selected range of paragraphs
496                 // of the same layout and depth.
497                 c.recordUndo();
498                 par.params().apply(params, par.layout());
499                 if (par.getDepth() != priordepth || par.layout() != priorlayout)
500                         setLabelWidthStringToSequence(c, params.labelWidthString());
501                 priordepth = par.getDepth();
502                 priorlayout = par.layout();
503         }
504 }
505
506
507 void Text::setParagraphs(Cursor const & cur, ParagraphParameters const & p)
508 {
509         LBUFERR(cur.text());
510
511         depth_type priordepth = -1;
512         Layout priorlayout;
513         Cursor c(cur.bv());
514         c.setCursor(cur.selectionBegin());
515         for ( ; c < cur.selectionEnd() ; ++c.pit()) {
516                 Paragraph & par = c.paragraph();
517                 // Changes to label width string apply to all paragraphs
518                 // with same layout in a sequence.
519                 // Do this only once for a selected range of paragraphs
520                 // of the same layout and depth.
521                 cur.recordUndo();
522                 par.params().apply(p, par.layout());
523                 if (par.getDepth() != priordepth || par.layout() != priorlayout)
524                         setLabelWidthStringToSequence(c,
525                                 par.params().labelWidthString());
526                 priordepth = par.getDepth();
527                 priorlayout = par.layout();
528         }
529 }
530
531
532 // this really should just insert the inset and not move the cursor.
533 void Text::insertInset(Cursor & cur, Inset * inset)
534 {
535         LBUFERR(this == cur.text());
536         LBUFERR(inset);
537         cur.paragraph().insertInset(cur.pos(), inset, cur.current_font,
538                 Change(cur.buffer()->params().track_changes
539                 ? Change::INSERTED : Change::UNCHANGED));
540 }
541
542
543 bool Text::setCursor(Cursor & cur, pit_type pit, pos_type pos,
544                         bool setfont, bool boundary)
545 {
546         TextMetrics const & tm = cur.bv().textMetrics(this);
547         bool const update_needed = !tm.contains(pit);
548         Cursor old = cur;
549         setCursorIntern(cur, pit, pos, setfont, boundary);
550         return cur.bv().checkDepm(cur, old) || update_needed;
551 }
552
553
554 void Text::setCursorIntern(Cursor & cur, pit_type pit, pos_type pos,
555                            bool setfont, bool boundary)
556 {
557         LBUFERR(this == cur.text());
558         cur.boundary(boundary);
559         cur.top().setPitPos(pit, pos);
560         if (setfont)
561                 cur.setCurrentFont();
562 }
563
564
565 bool Text::checkAndActivateInset(Cursor & cur, bool front)
566 {
567         if (front && cur.pos() == cur.lastpos())
568                 return false;
569         if (!front && cur.pos() == 0)
570                 return false;
571         Inset * inset = front ? cur.nextInset() : cur.prevInset();
572         if (!inset || !inset->editable())
573                 return false;
574         if (cur.selection() && cur.realAnchor().find(inset) == -1)
575                 return false;
576         /*
577          * Apparently, when entering an inset we are expected to be positioned
578          * *before* it in the containing paragraph, regardless of the direction
579          * from which we are entering. Otherwise, cursor placement goes awry,
580          * and when we exit from the beginning, we'll be placed *after* the
581          * inset.
582          */
583         if (!front)
584                 --cur.pos();
585         inset->edit(cur, front);
586         cur.setCurrentFont();
587         cur.boundary(false);
588         return true;
589 }
590
591
592 bool Text::checkAndActivateInsetVisual(Cursor & cur, bool movingForward, bool movingLeft)
593 {
594         if (cur.pos() == -1)
595                 return false;
596         if (cur.pos() == cur.lastpos())
597                 return false;
598         Paragraph & par = cur.paragraph();
599         Inset * inset = par.isInset(cur.pos()) ? par.getInset(cur.pos()) : nullptr;
600         if (!inset || !inset->editable())
601                 return false;
602         if (cur.selection() && cur.realAnchor().find(inset) == -1)
603                 return false;
604         inset->edit(cur, movingForward,
605                 movingLeft ? Inset::ENTRY_DIRECTION_RIGHT : Inset::ENTRY_DIRECTION_LEFT);
606         cur.setCurrentFont();
607         cur.boundary(false);
608         return true;
609 }
610
611
612 bool Text::cursorBackward(Cursor & cur)
613 {
614         // Tell BufferView to test for FitCursor in any case!
615         cur.screenUpdateFlags(Update::FitCursor);
616
617         // not at paragraph start?
618         if (cur.pos() > 0) {
619                 // if on right side of boundary (i.e. not at paragraph end, but line end)
620                 // -> skip it, i.e. set boundary to true, i.e. go only logically left
621                 // there are some exceptions to ignore this: lineseps, newlines, spaces
622 #if 0
623                 // some effectless debug code to see the values in the debugger
624                 bool bound = cur.boundary();
625                 int rowpos = cur.textRow().pos();
626                 int pos = cur.pos();
627                 bool sep = cur.paragraph().isSeparator(cur.pos() - 1);
628                 bool newline = cur.paragraph().isNewline(cur.pos() - 1);
629                 bool linesep = cur.paragraph().isLineSeparator(cur.pos() - 1);
630 #endif
631                 if (!cur.boundary() &&
632                                 cur.textRow().pos() == cur.pos() &&
633                                 !cur.paragraph().isLineSeparator(cur.pos() - 1) &&
634                                 !cur.paragraph().isNewline(cur.pos() - 1) &&
635                                 !cur.paragraph().isEnvSeparator(cur.pos() - 1) &&
636                                 !cur.paragraph().isSeparator(cur.pos() - 1)) {
637                         return setCursor(cur, cur.pit(), cur.pos(), true, true);
638                 }
639
640                 // go left and try to enter inset
641                 if (checkAndActivateInset(cur, false))
642                         return false;
643
644                 // normal character left
645                 return setCursor(cur, cur.pit(), cur.pos() - 1, true, false);
646         }
647
648         // move to the previous paragraph or do nothing
649         if (cur.pit() > 0) {
650                 Paragraph & par = getPar(cur.pit() - 1);
651                 pos_type lastpos = par.size();
652                 if (lastpos > 0 && par.isEnvSeparator(lastpos - 1))
653                         return setCursor(cur, cur.pit() - 1, lastpos - 1, true, false);
654                 else
655                         return setCursor(cur, cur.pit() - 1, lastpos, true, false);
656         }
657         return false;
658 }
659
660
661 bool Text::cursorVisLeft(Cursor & cur, bool skip_inset)
662 {
663         Cursor temp_cur = cur;
664         temp_cur.posVisLeft(skip_inset);
665         if (temp_cur.depth() > cur.depth()) {
666                 cur = temp_cur;
667                 return false;
668         }
669         return setCursor(cur, temp_cur.pit(), temp_cur.pos(),
670                 true, temp_cur.boundary());
671 }
672
673
674 bool Text::cursorVisRight(Cursor & cur, bool skip_inset)
675 {
676         Cursor temp_cur = cur;
677         temp_cur.posVisRight(skip_inset);
678         if (temp_cur.depth() > cur.depth()) {
679                 cur = temp_cur;
680                 return false;
681         }
682         return setCursor(cur, temp_cur.pit(), temp_cur.pos(),
683                 true, temp_cur.boundary());
684 }
685
686
687 bool Text::cursorForward(Cursor & cur)
688 {
689         // Tell BufferView to test for FitCursor in any case!
690         cur.screenUpdateFlags(Update::FitCursor);
691
692         // not at paragraph end?
693         if (cur.pos() != cur.lastpos()) {
694                 // in front of editable inset, i.e. jump into it?
695                 if (checkAndActivateInset(cur, true))
696                         return false;
697
698                 TextMetrics const & tm = cur.bv().textMetrics(this);
699                 // if left of boundary -> just jump to right side
700                 // but for RTL boundaries don't, because: abc|DDEEFFghi -> abcDDEEF|Fghi
701                 if (cur.boundary() && !tm.isRTLBoundary(cur.pit(), cur.pos()))
702                         return setCursor(cur, cur.pit(), cur.pos(), true, false);
703
704                 // next position is left of boundary,
705                 // but go to next line for special cases like space, newline, linesep
706 #if 0
707                 // some effectless debug code to see the values in the debugger
708                 int endpos = cur.textRow().endpos();
709                 int lastpos = cur.lastpos();
710                 int pos = cur.pos();
711                 bool linesep = cur.paragraph().isLineSeparator(cur.pos());
712                 bool newline = cur.paragraph().isNewline(cur.pos());
713                 bool sep = cur.paragraph().isSeparator(cur.pos());
714                 if (cur.pos() != cur.lastpos()) {
715                         bool linesep2 = cur.paragraph().isLineSeparator(cur.pos()+1);
716                         bool newline2 = cur.paragraph().isNewline(cur.pos()+1);
717                         bool sep2 = cur.paragraph().isSeparator(cur.pos()+1);
718                 }
719 #endif
720                 if (cur.textRow().endpos() == cur.pos() + 1) {
721                         if (cur.paragraph().isEnvSeparator(cur.pos()) &&
722                             cur.pos() + 1 == cur.lastpos() &&
723                             cur.pit() != cur.lastpit()) {
724                                 // move to next paragraph
725                                 return setCursor(cur, cur.pit() + 1, 0, true, false);
726                         } else if (cur.textRow().endpos() != cur.lastpos() &&
727                                    !cur.paragraph().isNewline(cur.pos()) &&
728                                    !cur.paragraph().isEnvSeparator(cur.pos()) &&
729                                    !cur.paragraph().isLineSeparator(cur.pos()) &&
730                                    !cur.paragraph().isSeparator(cur.pos())) {
731                                 return setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
732                         }
733                 }
734
735                 // in front of RTL boundary? Stay on this side of the boundary because:
736                 //   ab|cDDEEFFghi -> abc|DDEEFFghi
737                 if (tm.isRTLBoundary(cur.pit(), cur.pos() + 1))
738                         return setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
739
740                 // move right
741                 return setCursor(cur, cur.pit(), cur.pos() + 1, true, false);
742         }
743
744         // move to next paragraph
745         if (cur.pit() != cur.lastpit())
746                 return setCursor(cur, cur.pit() + 1, 0, true, false);
747         return false;
748 }
749
750
751 bool Text::cursorUpParagraph(Cursor & cur)
752 {
753         bool updated = false;
754         if (cur.pos() > 0)
755                 updated = setCursor(cur, cur.pit(), 0);
756         else if (cur.pit() != 0)
757                 updated = setCursor(cur, cur.pit() - 1, 0);
758         return updated;
759 }
760
761
762 bool Text::cursorDownParagraph(Cursor & cur)
763 {
764         bool updated = false;
765         if (cur.pit() != cur.lastpit())
766                 if (lyxrc.mac_like_cursor_movement)
767                         if (cur.pos() == cur.lastpos())
768                                 updated = setCursor(cur, cur.pit() + 1, getPar(cur.pit() + 1).size());
769                         else
770                                 updated = setCursor(cur, cur.pit(), cur.lastpos());
771                 else
772                         updated = setCursor(cur, cur.pit() + 1, 0);
773         else
774                 updated = setCursor(cur, cur.pit(), cur.lastpos());
775         return updated;
776 }
777
778 namespace {
779
780 /** delete num_spaces characters between from and to. Return the
781  * number of spaces that got physically deleted (not marked as
782  * deleted) */
783 int deleteSpaces(Paragraph & par, pos_type const from, pos_type to,
784                                   int num_spaces, bool const trackChanges)
785 {
786         if (num_spaces <= 0)
787                 return 0;
788
789         // First, delete spaces marked as inserted
790         int pos = from;
791         while (pos < to && num_spaces > 0) {
792                 Change const & change = par.lookupChange(pos);
793                 if (change.inserted() && change.currentAuthor()) {
794                         par.eraseChar(pos, trackChanges);
795                         --num_spaces;
796                         --to;
797                 } else
798                         ++pos;
799         }
800
801         // Then remove remaining spaces
802         int const psize = par.size();
803         par.eraseChars(from, from + num_spaces, trackChanges);
804         return psize - par.size();
805 }
806
807 }
808
809
810 bool Text::deleteEmptyParagraphMechanism(Cursor & cur,
811                 Cursor & old, bool & need_anchor_change)
812 {
813         //LYXERR(Debug::DEBUG, "DEPM: cur:\n" << cur << "old:\n" << old);
814
815         Paragraph & oldpar = old.paragraph();
816         bool const trackChanges = cur.buffer()->params().track_changes;
817         bool result = false;
818
819         // We do nothing if cursor did not move
820         if (cur.top() == old.top())
821                 return false;
822
823         // We do not do anything on read-only documents
824         if (cur.buffer()->isReadonly())
825                 return false;
826
827         // Whether a common inset is found and whether the cursor is still in
828         // the same paragraph (possibly nested).
829         int const depth = cur.find(&old.inset());
830         bool const same_par = depth != -1 && old.idx() == cur[depth].idx()
831                 && old.pit() == cur[depth].pit();
832
833         /*
834          * (1) If the chars around the old cursor were spaces and the
835          * paragraph is not in free spacing mode, delete some of them, but
836          * only if the cursor has really moved.
837          */
838
839         /* There are still some small problems that can lead to
840            double spaces stored in the document file or space at
841            the beginning of paragraphs(). This happens if you have
842            the cursor between two spaces and then save. Or if you
843            cut and paste and the selection has a space at the
844            beginning and then save right after the paste. (Lgb)
845         */
846         if (!oldpar.isFreeSpacing()) {
847                 // find range of spaces around cursors
848                 pos_type from = old.pos();
849                 while (from > 0
850                            && oldpar.isLineSeparator(from - 1)
851                            && !oldpar.isDeleted(from - 1))
852                         --from;
853                 pos_type to = old.pos();
854                 while (to < old.lastpos()
855                            && oldpar.isLineSeparator(to)
856                            && !oldpar.isDeleted(to))
857                         ++to;
858
859                 int num_spaces = to - from;
860                 // If we are not at the start of the paragraph, keep one space
861                 if (from != to && from > 0)
862                         --num_spaces;
863
864                 // If cursor is inside range, keep one additional space
865                 if (same_par && cur.pos() > from && cur.pos() < to)
866                         --num_spaces;
867
868                 // Remove spaces and adapt cursor.
869                 if (num_spaces > 0) {
870                         old.recordUndo();
871                         int const deleted =
872                                 deleteSpaces(oldpar, from, to, num_spaces, trackChanges);
873                         // correct cur position
874                         // FIXME: there can be other cursors pointing there, we should update them
875                         if (same_par) {
876                                 if (cur[depth].pos() >= to)
877                                         cur[depth].pos() -= deleted;
878                                 else if (cur[depth].pos() > from)
879                                         cur[depth].pos() = min(from + 1, old.lastpos());
880                                 need_anchor_change = true;
881                         }
882                         result = true;
883                 }
884         }
885
886         /*
887          * (2) If the paragraph where the cursor was is empty, delete it
888          */
889
890         // only do our other magic if we changed paragraph
891         if (same_par)
892                 return result;
893
894         // only do our magic if the paragraph is empty
895         if (!oldpar.empty())
896                 return result;
897
898         // don't delete anything if this is the ONLY paragraph!
899         if (old.lastpit() == 0)
900                 return result;
901
902         // Do not delete empty paragraphs with keepempty set.
903         if (oldpar.allowEmpty())
904                 return result;
905
906         // Delete old par.
907         old.recordUndo(max(old.pit() - 1, pit_type(0)),
908                        min(old.pit() + 1, old.lastpit()));
909         ParagraphList & plist = old.text()->paragraphs();
910         bool const soa = oldpar.params().startOfAppendix();
911         plist.erase(plist.iterator_at(old.pit()));
912         // do not lose start of appendix marker (bug 4212)
913         if (soa && old.pit() < pit_type(plist.size()))
914                 plist[old.pit()].params().startOfAppendix(true);
915
916         // see #warning (FIXME?) above
917         if (cur.depth() >= old.depth()) {
918                 CursorSlice & curslice = cur[old.depth() - 1];
919                 if (&curslice.inset() == &old.inset()
920                     && curslice.idx() == old.idx()
921                     && curslice.pit() > old.pit()) {
922                         --curslice.pit();
923                         // since a paragraph has been deleted, all the
924                         // insets after `old' have been copied and
925                         // their address has changed. Therefore we
926                         // need to `regenerate' cur. (JMarc)
927                         cur.updateInsets(&(cur.bottom().inset()));
928                         need_anchor_change = true;
929                 }
930         }
931
932         return true;
933 }
934
935
936 void Text::deleteEmptyParagraphMechanism(pit_type first, pit_type last, bool trackChanges)
937 {
938         pos_type last_pos = pars_[last].size() - 1;
939         deleteEmptyParagraphMechanism(first, last, 0, last_pos, trackChanges);
940 }
941
942
943 void Text::deleteEmptyParagraphMechanism(pit_type first, pit_type last,
944                                          pos_type first_pos, pos_type last_pos,
945                                          bool trackChanges)
946 {
947         LASSERT(first >= 0 && first <= last && last < (int) pars_.size(), return);
948
949         for (pit_type pit = first; pit <= last; ++pit) {
950                 Paragraph & par = pars_[pit];
951
952                 /*
953                  * (1) Delete consecutive spaces
954                  */
955                 if (!par.isFreeSpacing()) {
956                         pos_type from = (pit == first) ? first_pos : 0;
957                         pos_type to_pos = (pit == last) ? last_pos + 1 : par.size();
958                         while (from < to_pos) {
959                                 // skip non-spaces
960                                 while (from < par.size()
961                                            && (!par.isLineSeparator(from) || par.isDeleted(from)))
962                                         ++from;
963                                 // find string of spaces
964                                 pos_type to = from;
965                                 while (to < par.size()
966                                            && par.isLineSeparator(to) && !par.isDeleted(to))
967                                         ++to;
968                                 // empty? We are done
969                                 if (from == to)
970                                         break;
971
972                                 int num_spaces = to - from;
973
974                                 // If we are not at the extremity of the paragraph, keep one space
975                                 if (from != to && from > 0 && to < par.size())
976                                         --num_spaces;
977
978                                 // Remove spaces if needed
979                                 int const deleted = deleteSpaces(par, from , to, num_spaces, trackChanges);
980                                 from = to - deleted;
981                         }
982                 }
983
984                 /*
985                  * (2) Delete empty pragraphs
986                  */
987
988                 // don't delete anything if this is the only remaining paragraph
989                 // within the given range. Note: Text::acceptOrRejectChanges()
990                 // sets the cursor to 'first' after calling DEPM
991                 if (first == last)
992                         continue;
993
994                 // don't delete empty paragraphs with keepempty set
995                 if (par.allowEmpty())
996                         continue;
997
998                 if (par.empty() || (par.size() == 1 && par.isLineSeparator(0))) {
999                         pars_.erase(pars_.iterator_at(pit));
1000                         --pit;
1001                         --last;
1002                         continue;
1003                 }
1004         }
1005 }
1006
1007
1008 } // namespace lyx