]> git.lyx.org Git - lyx.git/blob - src/Text2.cpp
fix cursor movement (up/down) problem at the end of lines in insets and table cells.
[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 Dekel Tsur
15  * \author Jürgen Vigna
16  *
17  * Full author contact details are available in file CREDITS.
18  */
19
20 #include <config.h>
21
22 #include "Text.h"
23
24 #include "Buffer.h"
25 #include "buffer_funcs.h"
26 #include "BufferList.h"
27 #include "BufferParams.h"
28 #include "BufferView.h"
29 #include "bufferview_funcs.h"
30 #include "Bullet.h"
31 #include "CoordCache.h"
32 #include "Cursor.h"
33 #include "CutAndPaste.h"
34 #include "debug.h"
35 #include "DispatchResult.h"
36 #include "ErrorList.h"
37 #include "FuncRequest.h"
38 #include "gettext.h"
39 #include "Language.h"
40 #include "Color.h"
41 #include "LyXFunc.h"
42 #include "LyXRC.h"
43 #include "Row.h"
44 #include "Paragraph.h"
45 #include "TextMetrics.h"
46 #include "paragraph_funcs.h"
47 #include "ParagraphParameters.h"
48 #include "ParIterator.h"
49 #include "Server.h"
50 #include "ServerSocket.h"
51 #include "Undo.h"
52 #include "VSpace.h"
53
54 #include "frontends/FontMetrics.h"
55
56 #include "insets/InsetEnvironment.h"
57
58 #include "mathed/InsetMathHull.h"
59
60 #include "support/textutils.h"
61
62 #include <boost/current_function.hpp>
63
64 #include <sstream>
65
66
67 namespace lyx {
68
69 using std::endl;
70 using std::ostringstream;
71 using std::string;
72 using std::max;
73 using std::min;
74
75
76 Text::Text()
77         : current_font(Font::ALL_INHERIT),
78           background_color_(Color::background),
79           autoBreakRows_(false)
80 {}
81
82
83 bool Text::isMainText(Buffer const & buffer) const
84 {
85         return &buffer.text() == this;
86 }
87
88
89 //takes screen x,y coordinates
90 Inset * Text::checkInsetHit(BufferView & bv, int x, int y)
91 {
92         pit_type pit = getPitNearY(bv, y);
93         BOOST_ASSERT(pit != -1);
94
95         Paragraph const & par = pars_[pit];
96
97         LYXERR(Debug::DEBUG)
98                 << BOOST_CURRENT_FUNCTION
99                 << ": x: " << x
100                 << " y: " << y
101                 << "  pit: " << pit
102                 << endl;
103         InsetList::const_iterator iit = par.insetlist.begin();
104         InsetList::const_iterator iend = par.insetlist.end();
105         for (; iit != iend; ++iit) {
106                 Inset * inset = iit->inset;
107 #if 1
108                 LYXERR(Debug::DEBUG)
109                         << BOOST_CURRENT_FUNCTION
110                         << ": examining inset " << inset << endl;
111
112                 if (bv.coordCache().getInsets().has(inset))
113                         LYXERR(Debug::DEBUG)
114                                 << BOOST_CURRENT_FUNCTION
115                                 << ": xo: " << inset->xo(bv) << "..."
116                                 << inset->xo(bv) + inset->width()
117                                 << " yo: " << inset->yo(bv) - inset->ascent()
118                                 << "..."
119                                 << inset->yo(bv) + inset->descent()
120                                 << endl;
121                 else
122                         LYXERR(Debug::DEBUG)
123                                 << BOOST_CURRENT_FUNCTION
124                                 << ": inset has no cached position" << endl;
125 #endif
126                 if (inset->covers(bv, x, y)) {
127                         LYXERR(Debug::DEBUG)
128                                 << BOOST_CURRENT_FUNCTION
129                                 << ": Hit inset: " << inset << endl;
130                         return inset;
131                 }
132         }
133         LYXERR(Debug::DEBUG)
134                 << BOOST_CURRENT_FUNCTION
135                 << ": No inset hit. " << endl;
136         return 0;
137 }
138
139
140
141 // Gets the fully instantiated font at a given position in a paragraph
142 // Basically the same routine as Paragraph::getFont() in Paragraph.cpp.
143 // The difference is that this one is used for displaying, and thus we
144 // are allowed to make cosmetic improvements. For instance make footnotes
145 // smaller. (Asger)
146 Font Text::getFont(Buffer const & buffer, Paragraph const & par,
147                 pos_type const pos) const
148 {
149         BOOST_ASSERT(pos >= 0);
150
151         Layout_ptr const & layout = par.layout();
152 #ifdef WITH_WARNINGS
153 #warning broken?
154 #endif
155         BufferParams const & params = buffer.params();
156         pos_type const body_pos = par.beginOfBody();
157
158         // We specialize the 95% common case:
159         if (!par.getDepth()) {
160                 Font f = par.getFontSettings(params, pos);
161                 if (!isMainText(buffer))
162                         applyOuterFont(buffer, f);
163                 Font lf;
164                 Font rlf;
165                 if (layout->labeltype == LABEL_MANUAL && pos < body_pos) {
166                         lf = layout->labelfont;
167                         rlf = layout->reslabelfont;
168                 } else {
169                         lf = layout->font;
170                         rlf = layout->resfont;
171                 }
172                 // In case the default family has been customized
173                 if (lf.family() == Font::INHERIT_FAMILY)
174                         rlf.setFamily(params.getFont().family());
175                 return f.realize(rlf);
176         }
177
178         // The uncommon case need not be optimized as much
179         Font layoutfont;
180         if (pos < body_pos)
181                 layoutfont = layout->labelfont;
182         else
183                 layoutfont = layout->font;
184
185         Font font = par.getFontSettings(params, pos);
186         font.realize(layoutfont);
187
188         if (!isMainText(buffer))
189                 applyOuterFont(buffer, font);
190
191         // Find the pit value belonging to paragraph. This will not break
192         // even if pars_ would not be a vector anymore.
193         // Performance appears acceptable.
194
195         pit_type pit = pars_.size();
196         for (pit_type it = 0; it < pit; ++it)
197                 if (&pars_[it] == &par) {
198                         pit = it;
199                         break;
200                 }
201         // Realize against environment font information
202         // NOTE: the cast to pit_type should be removed when pit_type
203         // changes to a unsigned integer.
204         if (pit < pit_type(pars_.size()))
205                 font.realize(outerFont(pit, pars_));
206
207         // Realize with the fonts of lesser depth.
208         font.realize(params.getFont());
209
210         return font;
211 }
212
213 // There are currently two font mechanisms in LyX:
214 // 1. The font attributes in a lyxtext, and
215 // 2. The inset-specific font properties, defined in an inset's
216 // metrics() and draw() methods and handed down the inset chain through
217 // the pi/mi parameters, and stored locally in a lyxtext in font_.
218 // This is where the two are integrated in the final fully realized
219 // font.
220 void Text::applyOuterFont(Buffer const & buffer, Font & font) const {
221         Font lf(font_);
222         lf.reduce(buffer.params().getFont());
223         lf.realize(font);
224         lf.setLanguage(font.language());
225         font = lf;
226 }
227
228
229 Font Text::getLayoutFont(Buffer const & buffer, pit_type const pit) const
230 {
231         Layout_ptr const & layout = pars_[pit].layout();
232
233         if (!pars_[pit].getDepth())  {
234                 Font lf = layout->resfont;
235                 // In case the default family has been customized
236                 if (layout->font.family() == Font::INHERIT_FAMILY)
237                         lf.setFamily(buffer.params().getFont().family());
238                 return lf;
239         }
240
241         Font font = layout->font;
242         // Realize with the fonts of lesser depth.
243         //font.realize(outerFont(pit, paragraphs()));
244         font.realize(buffer.params().getFont());
245
246         return font;
247 }
248
249
250 Font Text::getLabelFont(Buffer const & buffer, Paragraph const & par) const
251 {
252         Layout_ptr const & layout = par.layout();
253
254         if (!par.getDepth()) {
255                 Font lf = layout->reslabelfont;
256                 // In case the default family has been customized
257                 if (layout->labelfont.family() == Font::INHERIT_FAMILY)
258                         lf.setFamily(buffer.params().getFont().family());
259                 return lf;
260         }
261
262         Font font = layout->labelfont;
263         // Realize with the fonts of lesser depth.
264         font.realize(buffer.params().getFont());
265
266         return font;
267 }
268
269
270 void Text::setCharFont(Buffer const & buffer, pit_type pit,
271                 pos_type pos, Font const & fnt)
272 {
273         Font font = fnt;
274         Layout_ptr const & layout = pars_[pit].layout();
275
276         // Get concrete layout font to reduce against
277         Font layoutfont;
278
279         if (pos < pars_[pit].beginOfBody())
280                 layoutfont = layout->labelfont;
281         else
282                 layoutfont = layout->font;
283
284         // Realize against environment font information
285         if (pars_[pit].getDepth()) {
286                 pit_type tp = pit;
287                 while (!layoutfont.resolved() &&
288                        tp != pit_type(paragraphs().size()) &&
289                        pars_[tp].getDepth()) {
290                         tp = outerHook(tp, paragraphs());
291                         if (tp != pit_type(paragraphs().size()))
292                                 layoutfont.realize(pars_[tp].layout()->font);
293                 }
294         }
295
296         // Inside inset, apply the inset's font attributes if any
297         // (charstyle!)
298         if (!isMainText(buffer))
299                 layoutfont.realize(font_);
300
301         layoutfont.realize(buffer.params().getFont());
302
303         // Now, reduce font against full layout font
304         font.reduce(layoutfont);
305
306         pars_[pit].setFont(pos, font);
307 }
308
309
310 // return past-the-last paragraph influenced by a layout change on pit
311 pit_type Text::undoSpan(pit_type pit)
312 {
313         pit_type end = paragraphs().size();
314         pit_type nextpit = pit + 1;
315         if (nextpit == end)
316                 return nextpit;
317         //because of parindents
318         if (!pars_[pit].getDepth())
319                 return boost::next(nextpit);
320         //because of depth constrains
321         for (; nextpit != end; ++pit, ++nextpit) {
322                 if (!pars_[pit].getDepth())
323                         break;
324         }
325         return nextpit;
326 }
327
328
329 void Text::setLayout(Buffer const & buffer, pit_type start, pit_type end,
330                 string const & layout)
331 {
332         BOOST_ASSERT(start != end);
333
334         BufferParams const & bufparams = buffer.params();
335         Layout_ptr const & lyxlayout = bufparams.getTextClass()[layout];
336
337         for (pit_type pit = start; pit != end; ++pit) {
338                 Paragraph & par = pars_[pit];
339                 par.applyLayout(lyxlayout);
340                 if (lyxlayout->margintype == MARGIN_MANUAL)
341                         par.setLabelWidthString(par.translateIfPossible(
342                                 lyxlayout->labelstring(), buffer.params()));
343         }
344 }
345
346
347 // set layout over selection and make a total rebreak of those paragraphs
348 void Text::setLayout(Cursor & cur, string const & layout)
349 {
350         BOOST_ASSERT(this == cur.text());
351         // special handling of new environment insets
352         BufferView & bv = cur.bv();
353         BufferParams const & params = bv.buffer()->params();
354         Layout_ptr const & lyxlayout = params.getTextClass()[layout];
355         if (lyxlayout->is_environment) {
356                 // move everything in a new environment inset
357                 LYXERR(Debug::DEBUG) << "setting layout " << layout << endl;
358                 lyx::dispatch(FuncRequest(LFUN_LINE_BEGIN));
359                 lyx::dispatch(FuncRequest(LFUN_LINE_END_SELECT));
360                 lyx::dispatch(FuncRequest(LFUN_CUT));
361                 Inset * inset = new InsetEnvironment(params, layout);
362                 insertInset(cur, inset);
363                 //inset->edit(cur, true);
364                 //lyx::dispatch(FuncRequest(LFUN_PASTE));
365                 return;
366         }
367
368         pit_type start = cur.selBegin().pit();
369         pit_type end = cur.selEnd().pit() + 1;
370         pit_type undopit = undoSpan(end - 1);
371         recUndo(cur, start, undopit - 1);
372         setLayout(cur.buffer(), start, end, layout);
373         updateLabels(cur.buffer());
374 }
375
376
377 static bool changeDepthAllowed(Text::DEPTH_CHANGE type,
378                         Paragraph const & par, int max_depth)
379 {
380         if (par.layout()->labeltype == LABEL_BIBLIO)
381                 return false;
382         int const depth = par.params().depth();
383         if (type == Text::INC_DEPTH && depth < max_depth)
384                 return true;
385         if (type == Text::DEC_DEPTH && depth > 0)
386                 return true;
387         return false;
388 }
389
390
391 bool Text::changeDepthAllowed(Cursor & cur, DEPTH_CHANGE type) const
392 {
393         BOOST_ASSERT(this == cur.text());
394         // this happens when selecting several cells in tabular (bug 2630)
395         if (cur.selBegin().idx() != cur.selEnd().idx())
396                 return false;
397
398         pit_type const beg = cur.selBegin().pit();
399         pit_type const end = cur.selEnd().pit() + 1;
400         int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
401
402         for (pit_type pit = beg; pit != end; ++pit) {
403                 if (lyx::changeDepthAllowed(type, pars_[pit], max_depth))
404                         return true;
405                 max_depth = pars_[pit].getMaxDepthAfter();
406         }
407         return false;
408 }
409
410
411 void Text::changeDepth(Cursor & cur, DEPTH_CHANGE type)
412 {
413         BOOST_ASSERT(this == cur.text());
414         pit_type const beg = cur.selBegin().pit();
415         pit_type const end = cur.selEnd().pit() + 1;
416         recordUndoSelection(cur);
417         int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
418
419         for (pit_type pit = beg; pit != end; ++pit) {
420                 Paragraph & par = pars_[pit];
421                 if (lyx::changeDepthAllowed(type, par, max_depth)) {
422                         int const depth = par.params().depth();
423                         if (type == INC_DEPTH)
424                                 par.params().depth(depth + 1);
425                         else
426                                 par.params().depth(depth - 1);
427                 }
428                 max_depth = par.getMaxDepthAfter();
429         }
430         // this handles the counter labels, and also fixes up
431         // depth values for follow-on (child) paragraphs
432         updateLabels(cur.buffer());
433 }
434
435
436 // set font over selection
437 void Text::setFont(Cursor & cur, Font const & font, bool toggleall)
438 {
439         BOOST_ASSERT(this == cur.text());
440         // if there is no selection just set the current_font
441         if (!cur.selection()) {
442                 // Determine basis font
443                 Font layoutfont;
444                 pit_type pit = cur.pit();
445                 if (cur.pos() < pars_[pit].beginOfBody())
446                         layoutfont = getLabelFont(cur.buffer(), pars_[pit]);
447                 else
448                         layoutfont = getLayoutFont(cur.buffer(), pit);
449
450                 // Update current font
451                 real_current_font.update(font,
452                                          cur.buffer().params().language,
453                                          toggleall);
454
455                 // Reduce to implicit settings
456                 current_font = real_current_font;
457                 current_font.reduce(layoutfont);
458                 // And resolve it completely
459                 real_current_font.realize(layoutfont);
460
461                 return;
462         }
463
464         // Ok, we have a selection.
465         recordUndoSelection(cur);
466
467         DocIterator dit = cur.selectionBegin();
468         DocIterator ditend = cur.selectionEnd();
469
470         BufferParams const & params = cur.buffer().params();
471
472         // Don't use forwardChar here as ditend might have
473         // pos() == lastpos() and forwardChar would miss it.
474         // Can't use forwardPos either as this descends into
475         // nested insets.
476         for (; dit != ditend; dit.forwardPosNoDescend()) {
477                 if (dit.pos() != dit.lastpos()) {
478                         Font f = getFont(cur.buffer(), dit.paragraph(), dit.pos());
479                         f.update(font, params.language, toggleall);
480                         setCharFont(cur.buffer(), dit.pit(), dit.pos(), f);
481                 }
482         }
483 }
484
485
486 // the cursor set functions have a special mechanism. When they
487 // realize you left an empty paragraph, they will delete it.
488
489 bool Text::cursorHome(Cursor & cur)
490 {
491         BOOST_ASSERT(this == cur.text());
492         ParagraphMetrics const & pm = cur.bv().parMetrics(this, cur.pit());
493         Row const & row = pm.getRow(cur.pos(),cur.boundary());
494         return setCursor(cur, cur.pit(), row.pos());
495 }
496
497
498 bool Text::cursorEnd(Cursor & cur)
499 {
500         BOOST_ASSERT(this == cur.text());
501         // if not on the last row of the par, put the cursor before
502         // the final space exept if I have a spanning inset or one string
503         // is so long that we force a break.
504         pos_type end = cur.textRow().endpos();
505         if (end == 0)
506                 // empty text, end-1 is no valid position
507                 return false;
508         bool boundary = false;
509         if (end != cur.lastpos()) {
510                 if (!cur.paragraph().isLineSeparator(end-1)
511                     && !cur.paragraph().isNewline(end-1))
512                         boundary = true;
513                 else
514                         --end;
515         }
516         return setCursor(cur, cur.pit(), end, true, boundary);
517 }
518
519
520 bool Text::cursorTop(Cursor & cur)
521 {
522         BOOST_ASSERT(this == cur.text());
523         return setCursor(cur, 0, 0);
524 }
525
526
527 bool Text::cursorBottom(Cursor & cur)
528 {
529         BOOST_ASSERT(this == cur.text());
530         return setCursor(cur, cur.lastpit(), boost::prior(paragraphs().end())->size());
531 }
532
533
534 void Text::toggleFree(Cursor & cur, Font const & font, bool toggleall)
535 {
536         BOOST_ASSERT(this == cur.text());
537         // If the mask is completely neutral, tell user
538         if (font == Font(Font::ALL_IGNORE)) {
539                 // Could only happen with user style
540                 cur.message(_("No font change defined."));
541                 return;
542         }
543
544         // Try implicit word selection
545         // If there is a change in the language the implicit word selection
546         // is disabled.
547         CursorSlice resetCursor = cur.top();
548         bool implicitSelection =
549                 font.language() == ignore_language
550                 && font.number() == Font::IGNORE
551                 && selectWordWhenUnderCursor(cur, WHOLE_WORD_STRICT);
552
553         // Set font
554         setFont(cur, font, toggleall);
555
556         // Implicit selections are cleared afterwards
557         // and cursor is set to the original position.
558         if (implicitSelection) {
559                 cur.clearSelection();
560                 cur.top() = resetCursor;
561                 cur.resetAnchor();
562         }
563 }
564
565
566 docstring Text::getStringToIndex(Cursor const & cur)
567 {
568         BOOST_ASSERT(this == cur.text());
569
570         docstring idxstring;
571         if (cur.selection())
572                 idxstring = cur.selectionAsString(false);
573         else {
574                 // Try implicit word selection. If there is a change
575                 // in the language the implicit word selection is
576                 // disabled.
577                 Cursor tmpcur = cur;
578                 selectWord(tmpcur, PREVIOUS_WORD);
579
580                 if (!tmpcur.selection())
581                         cur.message(_("Nothing to index!"));
582                 else if (tmpcur.selBegin().pit() != tmpcur.selEnd().pit())
583                         cur.message(_("Cannot index more than one paragraph!"));
584                 else
585                         idxstring = tmpcur.selectionAsString(false);
586         }
587
588         return idxstring;
589 }
590
591
592 void Text::setParagraph(Cursor & cur,
593                            Spacing const & spacing, LyXAlignment align,
594                            docstring const & labelwidthstring, bool noindent)
595 {
596         BOOST_ASSERT(cur.text());
597         // make sure that the depth behind the selection are restored, too
598         pit_type undopit = undoSpan(cur.selEnd().pit());
599         recUndo(cur, cur.selBegin().pit(), undopit - 1);
600
601         for (pit_type pit = cur.selBegin().pit(), end = cur.selEnd().pit();
602              pit <= end; ++pit) {
603                 Paragraph & par = pars_[pit];
604                 ParagraphParameters & params = par.params();
605                 params.spacing(spacing);
606
607                 // does the layout allow the new alignment?
608                 Layout_ptr const & layout = par.layout();
609
610                 if (align == LYX_ALIGN_LAYOUT)
611                         align = layout->align;
612                 if (align & layout->alignpossible) {
613                         if (align == layout->align)
614                                 params.align(LYX_ALIGN_LAYOUT);
615                         else
616                                 params.align(align);
617                 }
618                 par.setLabelWidthString(labelwidthstring);
619                 params.noindent(noindent);
620         }
621 }
622
623
624 // this really should just insert the inset and not move the cursor.
625 void Text::insertInset(Cursor & cur, Inset * inset)
626 {
627         BOOST_ASSERT(this == cur.text());
628         BOOST_ASSERT(inset);
629         cur.paragraph().insertInset(cur.pos(), inset, 
630                                     Change(cur.buffer().params().trackChanges ?
631                                            Change::INSERTED : Change::UNCHANGED));
632 }
633
634
635 // needed to insert the selection
636 void Text::insertStringAsLines(Cursor & cur, docstring const & str)
637 {
638         cur.buffer().insertStringAsLines(pars_, cur.pit(), cur.pos(),
639                                          current_font, str, autoBreakRows_);
640 }
641
642
643 // turn double CR to single CR, others are converted into one
644 // blank. Then insertStringAsLines is called
645 void Text::insertStringAsParagraphs(Cursor & cur, docstring const & str)
646 {
647         docstring linestr = str;
648         bool newline_inserted = false;
649
650         for (string::size_type i = 0, siz = linestr.size(); i < siz; ++i) {
651                 if (linestr[i] == '\n') {
652                         if (newline_inserted) {
653                                 // we know that \r will be ignored by
654                                 // insertStringAsLines. Of course, it is a dirty
655                                 // trick, but it works...
656                                 linestr[i - 1] = '\r';
657                                 linestr[i] = '\n';
658                         } else {
659                                 linestr[i] = ' ';
660                                 newline_inserted = true;
661                         }
662                 } else if (isPrintable(linestr[i])) {
663                         newline_inserted = false;
664                 }
665         }
666         insertStringAsLines(cur, linestr);
667 }
668
669
670 bool Text::setCursor(Cursor & cur, pit_type par, pos_type pos,
671                         bool setfont, bool boundary)
672 {
673         Cursor old = cur;
674         setCursorIntern(cur, par, pos, setfont, boundary);
675         return cur.bv().checkDepm(cur, old);
676 }
677
678
679 void Text::setCursor(CursorSlice & cur, pit_type par, pos_type pos)
680 {
681         BOOST_ASSERT(par != int(paragraphs().size()));
682         cur.pit() = par;
683         cur.pos() = pos;
684
685         // now some strict checking
686         Paragraph & para = getPar(par);
687
688         // None of these should happen, but we're scaredy-cats
689         if (pos < 0) {
690                 lyxerr << "dont like -1" << endl;
691                 BOOST_ASSERT(false);
692         }
693
694         if (pos > para.size()) {
695                 lyxerr << "dont like 1, pos: " << pos
696                        << " size: " << para.size()
697                        << " par: " << par << endl;
698                 BOOST_ASSERT(false);
699         }
700 }
701
702
703 void Text::setCursorIntern(Cursor & cur,
704                               pit_type par, pos_type pos, bool setfont, bool boundary)
705 {
706         BOOST_ASSERT(this == cur.text());
707         cur.boundary(boundary);
708         setCursor(cur.top(), par, pos);
709         if (setfont)
710                 setCurrentFont(cur);
711 }
712
713
714 void Text::setCurrentFont(Cursor & cur)
715 {
716         BOOST_ASSERT(this == cur.text());
717         pos_type pos = cur.pos();
718         Paragraph & par = cur.paragraph();
719
720         if (cur.boundary() && pos > 0)
721                 --pos;
722
723         if (pos > 0) {
724                 if (pos == cur.lastpos())
725                         --pos;
726                 else // potentional bug... BUG (Lgb)
727                         if (par.isSeparator(pos)) {
728                                 if (pos > cur.textRow().pos() &&
729                                     bidi.level(pos) % 2 ==
730                                     bidi.level(pos - 1) % 2)
731                                         --pos;
732                                 else if (pos + 1 < cur.lastpos())
733                                         ++pos;
734                         }
735         }
736
737         BufferParams const & bufparams = cur.buffer().params();
738         current_font = par.getFontSettings(bufparams, pos);
739         real_current_font = getFont(cur.buffer(), par, pos);
740
741         if (cur.pos() == cur.lastpos()
742             && bidi.isBoundary(cur.buffer(), par, cur.pos())
743             && !cur.boundary()) {
744                 Language const * lang = par.getParLanguage(bufparams);
745                 current_font.setLanguage(lang);
746                 current_font.setNumber(Font::OFF);
747                 real_current_font.setLanguage(lang);
748                 real_current_font.setNumber(Font::OFF);
749         }
750 }
751
752 // y is screen coordinate
753 pit_type Text::getPitNearY(BufferView & bv, int y) const
754 {
755         BOOST_ASSERT(!paragraphs().empty());
756         BOOST_ASSERT(bv.coordCache().getParPos().find(this) != bv.coordCache().getParPos().end());
757         CoordCache::InnerParPosCache const & cc = bv.coordCache().getParPos().find(this)->second;
758         LYXERR(Debug::DEBUG)
759                 << BOOST_CURRENT_FUNCTION
760                 << ": y: " << y << " cache size: " << cc.size()
761                 << endl;
762
763         // look for highest numbered paragraph with y coordinate less than given y
764         pit_type pit = 0;
765         int yy = -1;
766         CoordCache::InnerParPosCache::const_iterator it = cc.begin();
767         CoordCache::InnerParPosCache::const_iterator et = cc.end();
768         CoordCache::InnerParPosCache::const_iterator last = et; last--;
769
770         TextMetrics & tm = bv.textMetrics(this);
771         ParagraphMetrics const & pm = tm.parMetrics(it->first);
772
773         // If we are off-screen (before the visible part)
774         if (y < 0
775                 // and even before the first paragraph in the cache.
776                 && y < it->second.y_ - int(pm.ascent())) {
777                 //  and we are not at the first paragraph in the inset.
778                 if (it->first == 0)
779                         return 0;
780                 // then this is the paragraph we are looking for.
781                 pit = it->first - 1;
782                 // rebreak it and update the CoordCache.
783                 tm.redoParagraph(pit);
784                 bv.coordCache().parPos()[this][pit] =
785                         Point(0, it->second.y_ - pm.descent());
786                 return pit;
787         }
788
789         ParagraphMetrics const & pm_last = bv.parMetrics(this, last->first);
790
791         // If we are off-screen (after the visible part)
792         if (y > bv.workHeight()
793                 // and even after the first paragraph in the cache.
794                 && y >= last->second.y_ + int(pm_last.descent())) {
795                 pit = last->first + 1;
796                 //  and we are not at the last paragraph in the inset.
797                 if (pit == int(pars_.size()))
798                         return last->first;
799                 // then this is the paragraph we are looking for.
800                 // rebreak it and update the CoordCache.
801                 tm.redoParagraph(pit);
802                 bv.coordCache().parPos()[this][pit] =
803                         Point(0, last->second.y_ + pm_last.ascent());
804                 return pit;
805         }
806
807         for (; it != et; ++it) {
808                 LYXERR(Debug::DEBUG)
809                         << BOOST_CURRENT_FUNCTION
810                         << "  examining: pit: " << it->first
811                         << " y: " << it->second.y_
812                         << endl;
813
814                 ParagraphMetrics const & pm = bv.parMetrics(this, it->first);
815
816                 if (it->first >= pit && int(it->second.y_) - int(pm.ascent()) <= y) {
817                         pit = it->first;
818                         yy = it->second.y_;
819                 }
820         }
821
822         LYXERR(Debug::DEBUG)
823                 << BOOST_CURRENT_FUNCTION
824                 << ": found best y: " << yy << " for pit: " << pit
825                 << endl;
826
827         return pit;
828 }
829
830
831 Row const & Text::getRowNearY(BufferView const & bv, int y, pit_type pit) const
832 {
833         ParagraphMetrics const & pm = bv.parMetrics(this, pit);
834
835         int yy = bv.coordCache().get(this, pit).y_ - pm.ascent();
836         BOOST_ASSERT(!pm.rows().empty());
837         RowList::const_iterator rit = pm.rows().begin();
838         RowList::const_iterator const rlast = boost::prior(pm.rows().end());
839         for (; rit != rlast; yy += rit->height(), ++rit)
840                 if (yy + rit->height() > y)
841                         break;
842         return *rit;
843 }
844
845
846 // x,y are absolute screen coordinates
847 // sets cursor recursively descending into nested editable insets
848 Inset * Text::editXY(Cursor & cur, int x, int y)
849 {
850         if (lyxerr.debugging(Debug::WORKAREA)) {
851                 lyxerr << "Text::editXY(cur, " << x << ", " << y << ")" << std::endl;
852                 cur.bv().coordCache().dump();
853         }
854         pit_type pit = getPitNearY(cur.bv(), y);
855         BOOST_ASSERT(pit != -1);
856
857         Row const & row = getRowNearY(cur.bv(), y, pit);
858         bool bound = false;
859
860         TextMetrics const & tm = cur.bv().textMetrics(this);
861         int xx = x; // is modified by getColumnNearX
862         pos_type const pos = row.pos()
863                 + tm.getColumnNearX(pit, row, xx, bound);
864         cur.pit() = pit;
865         cur.pos() = pos;
866         cur.boundary(bound);
867         cur.x_target() = x;
868
869         // try to descend into nested insets
870         Inset * inset = checkInsetHit(cur.bv(), x, y);
871         //lyxerr << "inset " << inset << " hit at x: " << x << " y: " << y << endl;
872         if (!inset) {
873                 // Either we deconst editXY or better we move current_font
874                 // and real_current_font to Cursor
875                 setCurrentFont(cur);
876                 return 0;
877         }
878
879         Inset * insetBefore = pos? pars_[pit].getInset(pos - 1): 0;
880         //Inset * insetBehind = pars_[pit].getInset(pos);
881
882         // This should be just before or just behind the
883         // cursor position set above.
884         BOOST_ASSERT((pos != 0 && inset == insetBefore)
885                 || inset == pars_[pit].getInset(pos));
886
887         // Make sure the cursor points to the position before
888         // this inset.
889         if (inset == insetBefore)
890                 --cur.pos();
891
892         // Try to descend recursively inside the inset.
893         inset = inset->editXY(cur, x, y);
894
895         if (cur.top().text() == this)
896                 setCurrentFont(cur);
897         return inset;
898 }
899
900
901 bool Text::checkAndActivateInset(Cursor & cur, bool front)
902 {
903         if (cur.selection())
904                 return false;
905         if (cur.pos() == cur.lastpos())
906                 return false;
907         Inset * inset = cur.nextInset();
908         if (!isHighlyEditableInset(inset))
909                 return false;
910         inset->edit(cur, front);
911         return true;
912 }
913
914
915 bool Text::cursorLeft(Cursor & cur)
916 {
917         // Tell BufferView to test for FitCursor in any case!
918         cur.updateFlags(Update::FitCursor);
919
920         if (!cur.boundary() && cur.pos() > 0 &&
921             cur.textRow().pos() == cur.pos() &&
922             !cur.paragraph().isLineSeparator(cur.pos()-1) &&
923             !cur.paragraph().isNewline(cur.pos()-1)) {
924                 return setCursor(cur, cur.pit(), cur.pos(), true, true);
925         }
926         if (cur.pos() != 0) {
927                 bool updateNeeded = setCursor(cur, cur.pit(), cur.pos() - 1, true, false);
928                 if (!checkAndActivateInset(cur, false)) {
929                         /** FIXME: What's this cause purpose???
930                         bool boundary = cur.boundary();
931                         if (false && !boundary &&
932                             bidi.isBoundary(cur.buffer(), cur.paragraph(), cur.pos() + 1))
933                                 updateNeeded |=
934                                         setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
935                         */
936                 }
937                 return updateNeeded;
938         }
939
940         if (cur.pit() != 0) {
941                 // Steps into the paragraph above
942                 return setCursor(cur, cur.pit() - 1, getPar(cur.pit() - 1).size());
943         }
944         return false;
945 }
946
947
948 bool Text::cursorRight(Cursor & cur)
949 {
950         // Tell BufferView to test for FitCursor in any case!
951         cur.updateFlags(Update::FitCursor);
952
953         if (cur.pos() != cur.lastpos()) {
954                 if (cur.boundary())
955                         return setCursor(cur, cur.pit(), cur.pos(),
956                                          true, false);
957
958                 bool updateNeeded = false;
959                 if (!checkAndActivateInset(cur, true)) {
960                         if (cur.textRow().endpos() == cur.pos() + 1 &&
961                             cur.textRow().endpos() != cur.lastpos() &&
962                             !cur.paragraph().isLineSeparator(cur.pos()) &&
963                             !cur.paragraph().isNewline(cur.pos())) {
964                                 cur.boundary(true);
965                         }
966                         updateNeeded |= setCursor(cur, cur.pit(), cur.pos() + 1, true, cur.boundary());
967                         if (false && bidi.isBoundary(cur.buffer(), cur.paragraph(),
968                                                      cur.pos()))
969                                 updateNeeded |= setCursor(cur, cur.pit(), cur.pos(), true, true);
970                 }
971                 return updateNeeded;
972         }
973
974         if (cur.pit() != cur.lastpit())
975                 return setCursor(cur, cur.pit() + 1, 0);
976         return false;
977 }
978
979
980 bool Text::cursorUp(Cursor & cur)
981 {
982         // Tell BufferView to test for FitCursor in any case!
983         cur.updateFlags(Update::FitCursor);
984
985         TextMetrics const & tm = cur.bv().textMetrics(this);
986         ParagraphMetrics const & pm = tm.parMetrics(cur.pit());
987
988         int row;
989         if (cur.pos() && cur.boundary())
990                 row = pm.pos2row(cur.pos()-1);
991         else
992                 row = pm.pos2row(cur.pos());
993
994         int x = cur.targetX();
995         cur.setTargetX();
996         if (cur.pos() != pm.rows()[row].endpos() || x < cur.targetX())
997                 x = cur.targetX();
998
999         if (!cur.selection()) {
1000                 int const y = bv_funcs::getPos(cur.bv(), cur, cur.boundary()).y_;
1001                 Cursor old = cur;
1002                 // Go to middle of previous row. 16 found to work OK;
1003                 // 12 = top/bottom margin of display math
1004                 int const margin = 3 * InsetMathHull::displayMargin() / 2;
1005                 editXY(cur, x, y - pm.rows()[row].ascent() - margin);
1006                 cur.clearSelection();
1007
1008                 // This happens when you move out of an inset.
1009                 // And to give the DEPM the possibility of doing
1010                 // something we must provide it with two different
1011                 // cursors. (Lgb)
1012                 Cursor dummy = cur;
1013                 if (dummy == old)
1014                         ++dummy.pos();
1015
1016                 cur.bv().checkDepm(dummy, old);
1017                 return false;
1018         }
1019
1020         bool updateNeeded = false;
1021
1022         if (row > 0) {
1023                 updateNeeded |= setCursor(cur, cur.pit(),
1024                         tm.x2pos(cur.pit(), row - 1, x));
1025         } else if (cur.pit() > 0) {
1026                 --cur.pit();
1027                 //cannot use 'par' now
1028                 ParagraphMetrics const & pmcur = cur.bv().parMetrics(this, cur.pit());
1029                 updateNeeded |= setCursor(cur, cur.pit(),
1030                         tm.x2pos(cur.pit(), pmcur.rows().size() - 1, x));
1031         }
1032
1033         cur.x_target() = x;
1034
1035         return updateNeeded;
1036 }
1037
1038
1039 bool Text::cursorDown(Cursor & cur)
1040 {
1041         // Tell BufferView to test for FitCursor in any case!
1042         cur.updateFlags(Update::FitCursor);
1043
1044         TextMetrics const & tm = cur.bv().textMetrics(this);
1045         ParagraphMetrics const & pm = tm.parMetrics(cur.pit());
1046
1047         int row;
1048         if (cur.pos() && cur.boundary())
1049                 row = pm.pos2row(cur.pos()-1);
1050         else
1051                 row = pm.pos2row(cur.pos());
1052
1053         int x = cur.targetX();
1054         cur.setTargetX();
1055         if (cur.pos() != pm.rows()[row].endpos() || x < cur.targetX())
1056                 x = cur.targetX();
1057
1058         if (!cur.selection()) {
1059                 int const y = bv_funcs::getPos(cur.bv(), cur, cur.boundary()).y_;
1060                 Cursor old = cur;
1061                 // To middle of next row
1062                 int const margin = 3 * InsetMathHull::displayMargin() / 2;
1063                 editXY(cur, x, y + pm.rows()[row].descent() + margin);
1064                 cur.clearSelection();
1065
1066                 // This happens when you move out of an inset.
1067                 // And to give the DEPM the possibility of doing
1068                 // something we must provide it with two different
1069                 // cursors. (Lgb)
1070                 Cursor dummy = cur;
1071                 if (dummy == old)
1072                         ++dummy.pos();
1073                 
1074                 bool const changed = cur.bv().checkDepm(dummy, old);
1075
1076                 // Make sure that cur gets back whatever happened to dummy(Lgb)
1077                 if (changed)
1078                         cur = dummy;
1079
1080                 return false;
1081         }
1082
1083         bool updateNeeded = false;
1084
1085         if (row + 1 < int(pm.rows().size())) {
1086                 updateNeeded |= setCursor(cur, cur.pit(),
1087                         tm.x2pos(cur.pit(), row + 1, x));
1088         } else if (cur.pit() + 1 < int(paragraphs().size())) {
1089                 ++cur.pit();
1090                 updateNeeded |= setCursor(cur, cur.pit(),
1091                         tm.x2pos(cur.pit(), 0, x));
1092         }
1093
1094         cur.x_target() = x;
1095
1096         return updateNeeded;
1097 }
1098
1099
1100 bool Text::cursorUpParagraph(Cursor & cur)
1101 {
1102         bool updated = false;
1103         if (cur.pos() > 0)
1104                 updated = setCursor(cur, cur.pit(), 0);
1105         else if (cur.pit() != 0)
1106                 updated = setCursor(cur, cur.pit() - 1, 0);
1107         return updated;
1108 }
1109
1110
1111 bool Text::cursorDownParagraph(Cursor & cur)
1112 {
1113         bool updated = false;
1114         if (cur.pit() != cur.lastpit())
1115                 updated = setCursor(cur, cur.pit() + 1, 0);
1116         else
1117                 updated = setCursor(cur, cur.pit(), cur.lastpos());
1118         return updated;
1119 }
1120
1121
1122 // fix the cursor `cur' after a characters has been deleted at `where'
1123 // position. Called by deleteEmptyParagraphMechanism
1124 void Text::fixCursorAfterDelete(CursorSlice & cur, CursorSlice const & where)
1125 {
1126         // Do nothing if cursor is not in the paragraph where the
1127         // deletion occured,
1128         if (cur.pit() != where.pit())
1129                 return;
1130
1131         // If cursor position is after the deletion place update it
1132         if (cur.pos() > where.pos())
1133                 --cur.pos();
1134
1135         // Check also if we don't want to set the cursor on a spot behind the
1136         // pagragraph because we erased the last character.
1137         if (cur.pos() > cur.lastpos())
1138                 cur.pos() = cur.lastpos();
1139 }
1140
1141
1142 bool Text::deleteEmptyParagraphMechanism(Cursor & cur,
1143                 Cursor & old, bool & need_anchor_change)
1144 {
1145         //LYXERR(Debug::DEBUG) << "DEPM: cur:\n" << cur << "old:\n" << old << endl;
1146
1147         Paragraph & oldpar = old.paragraph();
1148
1149         // We allow all kinds of "mumbo-jumbo" when freespacing.
1150         if (oldpar.isFreeSpacing())
1151                 return false;
1152
1153         /* Ok I'll put some comments here about what is missing.
1154            There are still some small problems that can lead to
1155            double spaces stored in the document file or space at
1156            the beginning of paragraphs(). This happens if you have
1157            the cursor between to spaces and then save. Or if you
1158            cut and paste and the selection have a space at the
1159            beginning and then save right after the paste. (Lgb)
1160         */
1161
1162         // If old.pos() == 0 and old.pos()(1) == LineSeparator
1163         // delete the LineSeparator.
1164         // MISSING
1165
1166         // If old.pos() == 1 and old.pos()(0) == LineSeparator
1167         // delete the LineSeparator.
1168         // MISSING
1169
1170         bool const same_inset = &old.inset() == &cur.inset();
1171         bool const same_par = same_inset && old.pit() == cur.pit();
1172         bool const same_par_pos = same_par && old.pos() == cur.pos();
1173
1174         // If the chars around the old cursor were spaces, delete one of them.
1175         if (!same_par_pos) {
1176                 // Only if the cursor has really moved.
1177                 if (old.pos() > 0
1178                     && old.pos() < oldpar.size()
1179                     && oldpar.isLineSeparator(old.pos())
1180                     && oldpar.isLineSeparator(old.pos() - 1)
1181                     && !oldpar.isDeleted(old.pos() - 1)) {
1182                         oldpar.eraseChar(old.pos() - 1, cur.buffer().params().trackChanges);
1183 #ifdef WITH_WARNINGS
1184 #warning This will not work anymore when we have multiple views of the same buffer
1185 // In this case, we will have to correct also the cursors held by
1186 // other bufferviews. It will probably be easier to do that in a more
1187 // automated way in CursorSlice code. (JMarc 26/09/2001)
1188 #endif
1189                         // correct all cursor parts
1190                         if (same_par) {
1191                                 fixCursorAfterDelete(cur.top(), old.top());
1192                                 need_anchor_change = true;
1193                         }
1194                         return true;
1195                 }
1196         }
1197
1198         // only do our magic if we changed paragraph
1199         if (same_par)
1200                 return false;
1201
1202         // don't delete anything if this is the ONLY paragraph!
1203         if (old.lastpit() == 0)
1204                 return false;
1205
1206         // Do not delete empty paragraphs with keepempty set.
1207         if (oldpar.allowEmpty())
1208                 return false;
1209
1210         if (oldpar.empty() || (oldpar.size() == 1 && oldpar.isLineSeparator(0))) {
1211                 // Delete old par.
1212                 recordUndo(old, Undo::ATOMIC,
1213                            max(old.pit() - 1, pit_type(0)),
1214                            min(old.pit() + 1, old.lastpit()));
1215                 ParagraphList & plist = old.text()->paragraphs();
1216                 plist.erase(boost::next(plist.begin(), old.pit()));
1217
1218                 // see #warning above
1219                 if (cur.depth() >= old.depth()) {
1220                         CursorSlice & curslice = cur[old.depth() - 1];
1221                         if (&curslice.inset() == &old.inset()
1222                             && curslice.pit() > old.pit()) {
1223                                 --curslice.pit();
1224                                 // since a paragraph has been deleted, all the
1225                                 // insets after `old' have been copied and
1226                                 // their address has changed. Therefore we
1227                                 // need to `regenerate' cur. (JMarc)
1228                                 cur.updateInsets(&(cur.bottom().inset()));
1229                                 need_anchor_change = true;
1230                         }
1231                 }
1232                 return true;
1233         }
1234
1235         if (oldpar.stripLeadingSpaces(cur.buffer().params().trackChanges)) {
1236                 need_anchor_change = true;
1237                 // We return true here because the Paragraph contents changed and
1238                 // we need a redraw before further action is processed.
1239                 return true;
1240         }
1241
1242         return false;
1243 }
1244
1245
1246 void Text::deleteEmptyParagraphMechanism(pit_type first, pit_type last, bool trackChanges)
1247 {
1248         BOOST_ASSERT(first >= 0 && first <= last && last < (int) pars_.size());
1249
1250         for (pit_type pit = first; pit <= last; ++pit) {
1251                 Paragraph & par = pars_[pit];
1252
1253                 // We allow all kinds of "mumbo-jumbo" when freespacing.
1254                 if (par.isFreeSpacing())
1255                         continue;
1256
1257                 for (pos_type pos = 1; pos < par.size(); ++pos) {
1258                         if (par.isLineSeparator(pos) && par.isLineSeparator(pos - 1)
1259                             && !par.isDeleted(pos - 1)) {
1260                                 if (par.eraseChar(pos - 1, trackChanges)) {
1261                                         --pos;
1262                                 }
1263                         }
1264                 }
1265
1266                 // don't delete anything if this is the only remaining paragraph within the given range
1267                 // note: Text::acceptOrRejectChanges() sets the cursor to 'first' after calling DEPM 
1268                 if (first == last)
1269                         continue;
1270
1271                 // don't delete empty paragraphs with keepempty set
1272                 if (par.allowEmpty())
1273                         continue;
1274
1275                 if (par.empty() || (par.size() == 1 && par.isLineSeparator(0))) {
1276                         pars_.erase(boost::next(pars_.begin(), pit));
1277                         --pit;
1278                         --last;
1279                         continue;
1280                 }
1281
1282                 par.stripLeadingSpaces(trackChanges);
1283         }
1284 }
1285
1286
1287 void Text::recUndo(Cursor & cur, pit_type first, pit_type last) const
1288 {
1289         recordUndo(cur, Undo::ATOMIC, first, last);
1290 }
1291
1292
1293 void Text::recUndo(Cursor & cur, pit_type par) const
1294 {
1295         recordUndo(cur, Undo::ATOMIC, par, par);
1296 }
1297
1298 } // namespace lyx