]> git.lyx.org Git - lyx.git/blob - src/Text2.cpp
Change cursor shape for clickable math insets
[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         }
429 }
430
431
432 docstring Text::getStringForDialog(Cursor & cur)
433 {
434         LBUFERR(this == cur.text());
435
436         if (cur.selection())
437                 return cur.selectionAsString(false);
438
439         // Try implicit word selection. If there is a change
440         // in the language the implicit word selection is
441         // disabled.
442         selectWordWhenUnderCursor(cur, WHOLE_WORD);
443         docstring const & retval = cur.selectionAsString(false);
444         cur.clearSelection();
445         return retval;
446 }
447
448
449 void Text::setLabelWidthStringToSequence(Cursor const & cur,
450                 docstring const & s)
451 {
452         Cursor c = cur;
453         // Find first of same layout in sequence
454         while (!isFirstInSequence(c.pit())) {
455                 c.pit() = depthHook(c.pit(), c.paragraph().getDepth());
456         }
457
458         // now apply label width string to every par
459         // in sequence
460         depth_type const depth = c.paragraph().getDepth();
461         Layout const & layout = c.paragraph().layout();
462         for ( ; c.pit() <= c.lastpit() ; ++c.pit()) {
463                 while (c.paragraph().getDepth() > depth) {
464                         ++c.pit();
465                         if (c.pit() > c.lastpit())
466                                 return;
467                 }
468                 if (c.paragraph().getDepth() < depth)
469                         return;
470                 if (c.paragraph().layout() != layout)
471                         return;
472                 c.recordUndo();
473                 c.paragraph().setLabelWidthString(s);
474         }
475 }
476
477
478 void Text::setParagraphs(Cursor const & cur, docstring const & arg, bool merge)
479 {
480         LBUFERR(cur.text());
481
482         //FIXME UNICODE
483         string const argument = to_utf8(arg);
484         depth_type priordepth = -1;
485         Layout priorlayout;
486         Cursor c(cur.bv());
487         c.setCursor(cur.selectionBegin());
488         for ( ; c <= cur.selectionEnd() ; ++c.pit()) {
489                 Paragraph & par = c.paragraph();
490                 ParagraphParameters params = par.params();
491                 params.read(argument, merge);
492                 // Changes to label width string apply to all paragraphs
493                 // with same layout in a sequence.
494                 // Do this only once for a selected range of paragraphs
495                 // of the same layout and depth.
496                 c.recordUndo();
497                 par.params().apply(params, par.layout());
498                 if (par.getDepth() != priordepth || par.layout() != priorlayout)
499                         setLabelWidthStringToSequence(c, params.labelWidthString());
500                 priordepth = par.getDepth();
501                 priorlayout = par.layout();
502         }
503 }
504
505
506 void Text::setParagraphs(Cursor const & cur, ParagraphParameters const & p)
507 {
508         LBUFERR(cur.text());
509
510         depth_type priordepth = -1;
511         Layout priorlayout;
512         Cursor c(cur.bv());
513         c.setCursor(cur.selectionBegin());
514         for ( ; c < cur.selectionEnd() ; ++c.pit()) {
515                 Paragraph & par = c.paragraph();
516                 // Changes to label width string apply to all paragraphs
517                 // with same layout in a sequence.
518                 // Do this only once for a selected range of paragraphs
519                 // of the same layout and depth.
520                 cur.recordUndo();
521                 par.params().apply(p, par.layout());
522                 if (par.getDepth() != priordepth || par.layout() != priorlayout)
523                         setLabelWidthStringToSequence(c,
524                                 par.params().labelWidthString());
525                 priordepth = par.getDepth();
526                 priorlayout = par.layout();
527         }
528 }
529
530
531 // this really should just insert the inset and not move the cursor.
532 void Text::insertInset(Cursor & cur, Inset * inset)
533 {
534         LBUFERR(this == cur.text());
535         LBUFERR(inset);
536         cur.paragraph().insertInset(cur.pos(), inset, cur.current_font,
537                 Change(cur.buffer()->params().track_changes
538                 ? Change::INSERTED : Change::UNCHANGED));
539 }
540
541
542 bool Text::setCursor(Cursor & cur, pit_type pit, pos_type pos,
543                         bool setfont, bool boundary)
544 {
545         TextMetrics const & tm = cur.bv().textMetrics(this);
546         bool const update_needed = !tm.contains(pit);
547         Cursor old = cur;
548         setCursorIntern(cur, pit, pos, setfont, boundary);
549         return cur.bv().checkDepm(cur, old) || update_needed;
550 }
551
552
553 void Text::setCursorIntern(Cursor & cur, pit_type pit, pos_type pos,
554                            bool setfont, bool boundary)
555 {
556         LBUFERR(this == cur.text());
557         cur.boundary(boundary);
558         cur.top().setPitPos(pit, pos);
559         if (setfont)
560                 cur.setCurrentFont();
561 }
562
563
564 bool Text::checkAndActivateInset(Cursor & cur, bool front)
565 {
566         if (front && cur.pos() == cur.lastpos())
567                 return false;
568         if (!front && cur.pos() == 0)
569                 return false;
570         Inset * inset = front ? cur.nextInset() : cur.prevInset();
571         if (!inset || !inset->editable())
572                 return false;
573         if (cur.selection() && cur.realAnchor().find(inset) == -1)
574                 return false;
575         /*
576          * Apparently, when entering an inset we are expected to be positioned
577          * *before* it in the containing paragraph, regardless of the direction
578          * from which we are entering. Otherwise, cursor placement goes awry,
579          * and when we exit from the beginning, we'll be placed *after* the
580          * inset.
581          */
582         if (!front)
583                 --cur.pos();
584         inset->edit(cur, front);
585         cur.setCurrentFont();
586         cur.boundary(false);
587         return true;
588 }
589
590
591 bool Text::checkAndActivateInsetVisual(Cursor & cur, bool movingForward, bool movingLeft)
592 {
593         if (cur.pos() == -1)
594                 return false;
595         if (cur.pos() == cur.lastpos())
596                 return false;
597         Paragraph & par = cur.paragraph();
598         Inset * inset = par.isInset(cur.pos()) ? par.getInset(cur.pos()) : nullptr;
599         if (!inset || !inset->editable())
600                 return false;
601         if (cur.selection() && cur.realAnchor().find(inset) == -1)
602                 return false;
603         inset->edit(cur, movingForward,
604                 movingLeft ? Inset::ENTRY_DIRECTION_RIGHT : Inset::ENTRY_DIRECTION_LEFT);
605         cur.setCurrentFont();
606         cur.boundary(false);
607         return true;
608 }
609
610
611 bool Text::cursorBackward(Cursor & cur)
612 {
613         // Tell BufferView to test for FitCursor in any case!
614         cur.screenUpdateFlags(Update::FitCursor);
615
616         // not at paragraph start?
617         if (cur.pos() > 0) {
618                 // if on right side of boundary (i.e. not at paragraph end, but line end)
619                 // -> skip it, i.e. set boundary to true, i.e. go only logically left
620                 // there are some exceptions to ignore this: lineseps, newlines, spaces
621 #if 0
622                 // some effectless debug code to see the values in the debugger
623                 bool bound = cur.boundary();
624                 int rowpos = cur.textRow().pos();
625                 int pos = cur.pos();
626                 bool sep = cur.paragraph().isSeparator(cur.pos() - 1);
627                 bool newline = cur.paragraph().isNewline(cur.pos() - 1);
628                 bool linesep = cur.paragraph().isLineSeparator(cur.pos() - 1);
629 #endif
630                 if (!cur.boundary() &&
631                                 cur.textRow().pos() == cur.pos() &&
632                                 !cur.paragraph().isLineSeparator(cur.pos() - 1) &&
633                                 !cur.paragraph().isNewline(cur.pos() - 1) &&
634                                 !cur.paragraph().isEnvSeparator(cur.pos() - 1) &&
635                                 !cur.paragraph().isSeparator(cur.pos() - 1)) {
636                         return setCursor(cur, cur.pit(), cur.pos(), true, true);
637                 }
638
639                 // go left and try to enter inset
640                 if (checkAndActivateInset(cur, false))
641                         return false;
642
643                 // normal character left
644                 return setCursor(cur, cur.pit(), cur.pos() - 1, true, false);
645         }
646
647         // move to the previous paragraph or do nothing
648         if (cur.pit() > 0) {
649                 Paragraph & par = getPar(cur.pit() - 1);
650                 pos_type lastpos = par.size();
651                 if (lastpos > 0 && par.isEnvSeparator(lastpos - 1))
652                         return setCursor(cur, cur.pit() - 1, lastpos - 1, true, false);
653                 else
654                         return setCursor(cur, cur.pit() - 1, lastpos, true, false);
655         }
656         return false;
657 }
658
659
660 bool Text::cursorVisLeft(Cursor & cur, bool skip_inset)
661 {
662         Cursor temp_cur = cur;
663         temp_cur.posVisLeft(skip_inset);
664         if (temp_cur.depth() > cur.depth()) {
665                 cur = temp_cur;
666                 return false;
667         }
668         return setCursor(cur, temp_cur.pit(), temp_cur.pos(),
669                 true, temp_cur.boundary());
670 }
671
672
673 bool Text::cursorVisRight(Cursor & cur, bool skip_inset)
674 {
675         Cursor temp_cur = cur;
676         temp_cur.posVisRight(skip_inset);
677         if (temp_cur.depth() > cur.depth()) {
678                 cur = temp_cur;
679                 return false;
680         }
681         return setCursor(cur, temp_cur.pit(), temp_cur.pos(),
682                 true, temp_cur.boundary());
683 }
684
685
686 bool Text::cursorForward(Cursor & cur)
687 {
688         // Tell BufferView to test for FitCursor in any case!
689         cur.screenUpdateFlags(Update::FitCursor);
690
691         // not at paragraph end?
692         if (cur.pos() != cur.lastpos()) {
693                 // in front of editable inset, i.e. jump into it?
694                 if (checkAndActivateInset(cur, true))
695                         return false;
696
697                 TextMetrics const & tm = cur.bv().textMetrics(this);
698                 // if left of boundary -> just jump to right side
699                 // but for RTL boundaries don't, because: abc|DDEEFFghi -> abcDDEEF|Fghi
700                 if (cur.boundary() && !tm.isRTLBoundary(cur.pit(), cur.pos()))
701                         return setCursor(cur, cur.pit(), cur.pos(), true, false);
702
703                 // next position is left of boundary,
704                 // but go to next line for special cases like space, newline, linesep
705 #if 0
706                 // some effectless debug code to see the values in the debugger
707                 int endpos = cur.textRow().endpos();
708                 int lastpos = cur.lastpos();
709                 int pos = cur.pos();
710                 bool linesep = cur.paragraph().isLineSeparator(cur.pos());
711                 bool newline = cur.paragraph().isNewline(cur.pos());
712                 bool sep = cur.paragraph().isSeparator(cur.pos());
713                 if (cur.pos() != cur.lastpos()) {
714                         bool linesep2 = cur.paragraph().isLineSeparator(cur.pos()+1);
715                         bool newline2 = cur.paragraph().isNewline(cur.pos()+1);
716                         bool sep2 = cur.paragraph().isSeparator(cur.pos()+1);
717                 }
718 #endif
719                 if (cur.textRow().endpos() == cur.pos() + 1) {
720                         if (cur.paragraph().isEnvSeparator(cur.pos()) &&
721                             cur.pos() + 1 == cur.lastpos() &&
722                             cur.pit() != cur.lastpit()) {
723                                 // move to next paragraph
724                                 return setCursor(cur, cur.pit() + 1, 0, true, false);
725                         } else if (cur.textRow().endpos() != cur.lastpos() &&
726                                    !cur.paragraph().isNewline(cur.pos()) &&
727                                    !cur.paragraph().isEnvSeparator(cur.pos()) &&
728                                    !cur.paragraph().isLineSeparator(cur.pos()) &&
729                                    !cur.paragraph().isSeparator(cur.pos())) {
730                                 return setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
731                         }
732                 }
733
734                 // in front of RTL boundary? Stay on this side of the boundary because:
735                 //   ab|cDDEEFFghi -> abc|DDEEFFghi
736                 if (tm.isRTLBoundary(cur.pit(), cur.pos() + 1))
737                         return setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
738
739                 // move right
740                 return setCursor(cur, cur.pit(), cur.pos() + 1, true, false);
741         }
742
743         // move to next paragraph
744         if (cur.pit() != cur.lastpit())
745                 return setCursor(cur, cur.pit() + 1, 0, true, false);
746         return false;
747 }
748
749
750 bool Text::cursorUpParagraph(Cursor & cur)
751 {
752         bool updated = false;
753         if (cur.pos() > 0)
754                 updated = setCursor(cur, cur.pit(), 0);
755         else if (cur.pit() != 0)
756                 updated = setCursor(cur, cur.pit() - 1, 0);
757         return updated;
758 }
759
760
761 bool Text::cursorDownParagraph(Cursor & cur)
762 {
763         bool updated = false;
764         if (cur.pit() != cur.lastpit())
765                 if (lyxrc.mac_like_cursor_movement)
766                         if (cur.pos() == cur.lastpos())
767                                 updated = setCursor(cur, cur.pit() + 1, getPar(cur.pit() + 1).size());
768                         else
769                                 updated = setCursor(cur, cur.pit(), cur.lastpos());
770                 else
771                         updated = setCursor(cur, cur.pit() + 1, 0);
772         else
773                 updated = setCursor(cur, cur.pit(), cur.lastpos());
774         return updated;
775 }
776
777 namespace {
778
779 /** delete num_spaces characters between from and to. Return the
780  * number of spaces that got physically deleted (not marked as
781  * deleted) */
782 int deleteSpaces(Paragraph & par, pos_type const from, pos_type to,
783                                   int num_spaces, bool const trackChanges)
784 {
785         if (num_spaces <= 0)
786                 return 0;
787
788         // First, delete spaces marked as inserted
789         int pos = from;
790         while (pos < to && num_spaces > 0) {
791                 Change const & change = par.lookupChange(pos);
792                 if (change.inserted() && change.currentAuthor()) {
793                         par.eraseChar(pos, trackChanges);
794                         --num_spaces;
795                         --to;
796                 } else
797                         ++pos;
798         }
799
800         // Then remove remaining spaces
801         int const psize = par.size();
802         par.eraseChars(from, from + num_spaces, trackChanges);
803         return psize - par.size();
804 }
805
806 }
807
808
809 bool Text::deleteEmptyParagraphMechanism(Cursor & cur,
810                 Cursor & old, bool & need_anchor_change)
811 {
812         //LYXERR(Debug::DEBUG, "DEPM: cur:\n" << cur << "old:\n" << old);
813
814         Paragraph & oldpar = old.paragraph();
815         bool const trackChanges = cur.buffer()->params().track_changes;
816         bool result = false;
817
818         // We do nothing if cursor did not move
819         if (cur.top() == old.top())
820                 return false;
821
822         // We do not do anything on read-only documents
823         if (cur.buffer()->isReadonly())
824                 return false;
825
826         // Whether a common inset is found and whether the cursor is still in
827         // the same paragraph (possibly nested).
828         int const depth = cur.find(&old.inset());
829         bool const same_par = depth != -1 && old.idx() == cur[depth].idx()
830                 && old.pit() == cur[depth].pit();
831
832         /*
833          * (1) If the chars around the old cursor were spaces and the
834          * paragraph is not in free spacing mode, delete some of them, but
835          * only if the cursor has really moved.
836          */
837
838         /* There are still some small problems that can lead to
839            double spaces stored in the document file or space at
840            the beginning of paragraphs(). This happens if you have
841            the cursor between two spaces and then save. Or if you
842            cut and paste and the selection has a space at the
843            beginning and then save right after the paste. (Lgb)
844         */
845         if (!oldpar.isFreeSpacing()) {
846                 // find range of spaces around cursors
847                 pos_type from = old.pos();
848                 while (from > 0
849                            && oldpar.isLineSeparator(from - 1)
850                            && !oldpar.isDeleted(from - 1))
851                         --from;
852                 pos_type to = old.pos();
853                 while (to < old.lastpos()
854                            && oldpar.isLineSeparator(to)
855                            && !oldpar.isDeleted(to))
856                         ++to;
857
858                 int num_spaces = to - from;
859                 // If we are not at the start of the paragraph, keep one space
860                 if (from != to && from > 0)
861                         --num_spaces;
862
863                 // If cursor is inside range, keep one additional space
864                 if (same_par && cur.pos() > from && cur.pos() < to)
865                         --num_spaces;
866
867                 // Remove spaces and adapt cursor.
868                 if (num_spaces > 0) {
869                         old.recordUndo();
870                         int const deleted =
871                                 deleteSpaces(oldpar, from, to, num_spaces, trackChanges);
872                         // correct cur position
873                         // FIXME: there can be other cursors pointing there, we should update them
874                         if (same_par) {
875                                 if (cur[depth].pos() >= to)
876                                         cur[depth].pos() -= deleted;
877                                 else if (cur[depth].pos() > from)
878                                         cur[depth].pos() = min(from + 1, old.lastpos());
879                                 need_anchor_change = true;
880                         }
881                         result = true;
882                 }
883         }
884
885         /*
886          * (2) If the paragraph where the cursor was is empty, delete it
887          */
888
889         // only do our other magic if we changed paragraph
890         if (same_par)
891                 return result;
892
893         // only do our magic if the paragraph is empty
894         if (!oldpar.empty())
895                 return result;
896
897         // don't delete anything if this is the ONLY paragraph!
898         if (old.lastpit() == 0)
899                 return result;
900
901         // Do not delete empty paragraphs with keepempty set.
902         if (oldpar.allowEmpty())
903                 return result;
904
905         // Delete old par.
906         old.recordUndo(max(old.pit() - 1, pit_type(0)),
907                        min(old.pit() + 1, old.lastpit()));
908         ParagraphList & plist = old.text()->paragraphs();
909         bool const soa = oldpar.params().startOfAppendix();
910         plist.erase(plist.iterator_at(old.pit()));
911         // do not lose start of appendix marker (bug 4212)
912         if (soa && old.pit() < pit_type(plist.size()))
913                 plist[old.pit()].params().startOfAppendix(true);
914
915         // see #warning (FIXME?) above
916         if (cur.depth() >= old.depth()) {
917                 CursorSlice & curslice = cur[old.depth() - 1];
918                 if (&curslice.inset() == &old.inset()
919                     && curslice.idx() == old.idx()
920                     && curslice.pit() > old.pit()) {
921                         --curslice.pit();
922                         // since a paragraph has been deleted, all the
923                         // insets after `old' have been copied and
924                         // their address has changed. Therefore we
925                         // need to `regenerate' cur. (JMarc)
926                         cur.updateInsets(&(cur.bottom().inset()));
927                         need_anchor_change = true;
928                 }
929         }
930
931         return true;
932 }
933
934
935 void Text::deleteEmptyParagraphMechanism(pit_type first, pit_type last, bool trackChanges)
936 {
937         pos_type last_pos = static_cast<pos_type>(pars_[last].size() - 1);
938         deleteEmptyParagraphMechanism(first, last, 0, last_pos, trackChanges);
939 }
940
941
942 void Text::deleteEmptyParagraphMechanism(pit_type first, pit_type last,
943                                          pos_type first_pos, pos_type last_pos,
944                                          bool trackChanges)
945 {
946         LASSERT(first >= 0 && first <= last && last < (int) pars_.size(), return);
947
948         for (pit_type pit = first; pit <= last; ++pit) {
949                 Paragraph & par = pars_[pit];
950
951                 /*
952                  * (1) Delete consecutive spaces
953                  */
954                 if (!par.isFreeSpacing()) {
955                         pos_type from = (pit == first) ? first_pos : 0;
956                         pos_type to_pos = (pit == last) ? last_pos + 1 : par.size();
957                         while (from < to_pos) {
958                                 // skip non-spaces
959                                 while (from < par.size()
960                                            && (!par.isLineSeparator(from) || par.isDeleted(from)))
961                                         ++from;
962                                 // find string of spaces
963                                 pos_type to = from;
964                                 while (to < par.size()
965                                            && par.isLineSeparator(to) && !par.isDeleted(to))
966                                         ++to;
967                                 // empty? We are done
968                                 if (from == to)
969                                         break;
970
971                                 int num_spaces = to - from;
972
973                                 // If we are not at the extremity of the paragraph, keep one space
974                                 if (from != to && from > 0 && to < par.size())
975                                         --num_spaces;
976
977                                 // Remove spaces if needed
978                                 int const deleted = deleteSpaces(par, from , to, num_spaces, trackChanges);
979                                 from = to - deleted;
980                         }
981                 }
982
983                 /*
984                  * (2) Delete empty pragraphs
985                  */
986
987                 // don't delete anything if this is the only remaining paragraph
988                 // within the given range. Note: Text::acceptOrRejectChanges()
989                 // sets the cursor to 'first' after calling DEPM
990                 if (first == last)
991                         continue;
992
993                 // don't delete empty paragraphs with keepempty set
994                 if (par.allowEmpty())
995                         continue;
996
997                 if (par.empty() || (par.size() == 1 && par.isLineSeparator(0))) {
998                         pars_.erase(pars_.iterator_at(pit));
999                         --pit;
1000                         --last;
1001                         continue;
1002                 }
1003         }
1004 }
1005
1006
1007 } // namespace lyx