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