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