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