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