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