]> git.lyx.org Git - lyx.git/blob - src/text2.C
87795a7eec40c03d226ff8af4452ee528da33cb7
[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 (!cur.paragraph().isLineSeparator(end-1) &&
502             !cur.paragraph().isNewline(end-1))
503         {
504                 boundary = true;
505         } else if (end != cur.lastpos())
506                 --end;
507         setCursor(cur, cur.pit(), end, true, boundary);
508 }
509
510
511 void LyXText::cursorTop(LCursor & cur)
512 {
513         BOOST_ASSERT(this == cur.text());
514         setCursor(cur, 0, 0);
515 }
516
517
518 void LyXText::cursorBottom(LCursor & cur)
519 {
520         BOOST_ASSERT(this == cur.text());
521         setCursor(cur, cur.lastpit(), boost::prior(paragraphs().end())->size());
522 }
523
524
525 void LyXText::toggleFree(LCursor & cur, LyXFont const & font, bool toggleall)
526 {
527         BOOST_ASSERT(this == cur.text());
528         // If the mask is completely neutral, tell user
529         if (font == LyXFont(LyXFont::ALL_IGNORE)) {
530                 // Could only happen with user style
531                 cur.message(_("No font change defined. "
532                         "Use Character under the Layout menu to define font change."));
533                 return;
534         }
535
536         // Try implicit word selection
537         // If there is a change in the language the implicit word selection
538         // is disabled.
539         CursorSlice resetCursor = cur.top();
540         bool implicitSelection =
541                 font.language() == ignore_language
542                 && font.number() == LyXFont::IGNORE
543                 && selectWordWhenUnderCursor(cur, lyx::WHOLE_WORD_STRICT);
544
545         // Set font
546         setFont(cur, font, toggleall);
547
548         // Implicit selections are cleared afterwards
549         // and cursor is set to the original position.
550         if (implicitSelection) {
551                 cur.clearSelection();
552                 cur.top() = resetCursor;
553                 cur.resetAnchor();
554         }
555 }
556
557
558 string LyXText::getStringToIndex(LCursor const & cur)
559 {
560         BOOST_ASSERT(this == cur.text());
561
562         string idxstring;
563         if (cur.selection()) {
564                 idxstring = cur.selectionAsString(false);
565         } else {
566                 // Try implicit word selection. If there is a change
567                 // in the language the implicit word selection is
568                 // disabled.
569                 LCursor tmpcur = cur;
570                 selectWord(tmpcur, lyx::PREVIOUS_WORD);
571
572                 if (!tmpcur.selection())
573                         cur.message(_("Nothing to index!"));
574                 else if (tmpcur.selBegin().pit() != tmpcur.selEnd().pit())
575                         cur.message(_("Cannot index more than one paragraph!"));
576                 else
577                         idxstring = tmpcur.selectionAsString(false);
578         }
579
580         return idxstring;
581 }
582
583
584 void LyXText::setParagraph(LCursor & cur,
585         Spacing const & spacing, LyXAlignment align,
586         string const & labelwidthstring, bool noindent)
587 {
588         BOOST_ASSERT(cur.text());
589         // make sure that the depth behind the selection are restored, too
590         pit_type undopit = undoSpan(cur.selEnd().pit());
591         recUndo(cur.selBegin().pit(), undopit - 1);
592
593         for (pit_type pit = cur.selBegin().pit(), end = cur.selEnd().pit();
594                         pit <= end; ++pit) {
595                 Paragraph & par = pars_[pit];
596                 ParagraphParameters & params = par.params();
597                 params.spacing(spacing);
598
599                 // does the layout allow the new alignment?
600                 LyXLayout_ptr const & layout = par.layout();
601
602                 if (align == LYX_ALIGN_LAYOUT)
603                         align = layout->align;
604                 if (align & layout->alignpossible) {
605                         if (align == layout->align)
606                                 params.align(LYX_ALIGN_LAYOUT);
607                         else
608                                 params.align(align);
609                 }
610                 par.setLabelWidthString(labelwidthstring);
611                 params.noindent(noindent);
612         }
613 }
614
615
616 // this really should just insert the inset and not move the cursor.
617 void LyXText::insertInset(LCursor & cur, InsetBase * inset)
618 {
619         BOOST_ASSERT(this == cur.text());
620         BOOST_ASSERT(inset);
621         cur.paragraph().insertInset(cur.pos(), inset);
622 }
623
624
625 // needed to insert the selection
626 void LyXText::insertStringAsLines(LCursor & cur, string const & str)
627 {
628         cur.buffer().insertStringAsLines(pars_, cur.pit(), cur.pos(), 
629                 current_font, str, autoBreakRows_);
630 }
631
632
633 // turn double CR to single CR, others are converted into one
634 // blank. Then insertStringAsLines is called
635 void LyXText::insertStringAsParagraphs(LCursor & cur, string const & str)
636 {
637         string linestr = str;
638         bool newline_inserted = false;
639
640         for (string::size_type i = 0, siz = linestr.size(); i < siz; ++i) {
641                 if (linestr[i] == '\n') {
642                         if (newline_inserted) {
643                                 // we know that \r will be ignored by
644                                 // insertStringAsLines. Of course, it is a dirty
645                                 // trick, but it works...
646                                 linestr[i - 1] = '\r';
647                                 linestr[i] = '\n';
648                         } else {
649                                 linestr[i] = ' ';
650                                 newline_inserted = true;
651                         }
652                 } else if (IsPrintable(linestr[i])) {
653                         newline_inserted = false;
654                 }
655         }
656         insertStringAsLines(cur, linestr);
657 }
658
659
660 bool LyXText::setCursor(LCursor & cur, pit_type par, pos_type pos,
661         bool setfont, bool boundary)
662 {
663         LCursor old = cur;
664         setCursorIntern(cur, par, pos, setfont, boundary);
665         return deleteEmptyParagraphMechanism(cur, old);
666 }
667
668
669 void LyXText::setCursor(CursorSlice & cur, pit_type par,
670         pos_type pos, bool boundary)
671 {
672         BOOST_ASSERT(par != int(paragraphs().size()));
673         cur.pit() = par;
674         cur.pos() = pos;
675
676         // now some strict checking
677         Paragraph & para = getPar(par);
678
679         // None of these should happen, but we're scaredy-cats
680         if (pos < 0) {
681                 lyxerr << "dont like -1" << endl;
682                 BOOST_ASSERT(false);
683         }
684
685         if (pos > para.size()) {
686                 lyxerr << "dont like 1, pos: " << pos
687                        << " size: " << para.size()
688                        << " par: " << par << endl;
689                 BOOST_ASSERT(false);
690         }
691 }
692
693
694 void LyXText::setCursorIntern(LCursor & cur,
695         pit_type par, pos_type pos, bool setfont, bool boundary)
696 {
697         cur.boundary(boundary);
698         setCursor(cur.top(), par, pos, boundary);
699         cur.setTargetX();
700         if (setfont)
701                 setCurrentFont(cur);
702 }
703
704
705 void LyXText::setCurrentFont(LCursor & cur)
706 {
707         BOOST_ASSERT(this == cur.text());
708         pos_type pos = cur.pos();
709         Paragraph & par = cur.paragraph();
710
711         if (cur.boundary() && pos > 0)
712                 --pos;
713
714         if (pos > 0) {
715                 if (pos == cur.lastpos())
716                         --pos;
717                 else // potentional bug... BUG (Lgb)
718                         if (par.isSeparator(pos)) {
719                                 if (pos > cur.textRow().pos() &&
720                                     bidi.level(pos) % 2 ==
721                                     bidi.level(pos - 1) % 2)
722                                         --pos;
723                                 else if (pos + 1 < cur.lastpos())
724                                         ++pos;
725                         }
726         }
727
728         BufferParams const & bufparams = cur.buffer().params();
729         current_font = par.getFontSettings(bufparams, pos);
730         real_current_font = getFont(par, pos);
731
732         if (cur.pos() == cur.lastpos()
733             && bidi.isBoundary(cur.buffer(), par, cur.pos())
734             && !cur.boundary()) {
735                 Language const * lang = par.getParLanguage(bufparams);
736                 current_font.setLanguage(lang);
737                 current_font.setNumber(LyXFont::OFF);
738                 real_current_font.setLanguage(lang);
739                 real_current_font.setNumber(LyXFont::OFF);
740         }
741 }
742
743
744 // x is an absolute screen coord
745 // returns the column near the specified x-coordinate of the row
746 // x is set to the real beginning of this column
747 pos_type LyXText::getColumnNearX(pit_type const pit,
748         Row const & row, int & x, bool & boundary) const
749 {
750         int const xo = theCoords.get(this, pit).x_;
751         x -= xo;
752         RowMetrics const r = computeRowMetrics(pit, row);
753         Paragraph const & par = pars_[pit];
754
755         pos_type vc = row.pos();
756         pos_type end = row.endpos();
757         pos_type c = 0;
758         LyXLayout_ptr const & layout = par.layout();
759
760         bool left_side = false;
761
762         pos_type body_pos = par.beginOfBody();
763
764         double tmpx = r.x;
765         double last_tmpx = tmpx;
766
767         if (body_pos > 0 &&
768             (body_pos > end || !par.isLineSeparator(body_pos - 1)))
769                 body_pos = 0;
770
771         // check for empty row
772         if (vc == end) {
773                 x = int(tmpx) + xo;
774                 return 0;
775         }
776
777         while (vc < end && tmpx <= x) {
778                 c = bidi.vis2log(vc);
779                 last_tmpx = tmpx;
780                 if (body_pos > 0 && c == body_pos - 1) {
781                         tmpx += r.label_hfill +
782                                 font_metrics::width(layout->labelsep, getLabelFont(par));
783                         if (par.isLineSeparator(body_pos - 1))
784                                 tmpx -= singleWidth(par, body_pos - 1);
785                 }
786
787                 if (hfillExpansion(par, row, c)) {
788                         tmpx += singleWidth(par, c);
789                         if (c >= body_pos)
790                                 tmpx += r.hfill;
791                         else
792                                 tmpx += r.label_hfill;
793                 } else if (par.isSeparator(c)) {
794                         tmpx += singleWidth(par, c);
795                         if (c >= body_pos)
796                                 tmpx += r.separator;
797                 } else {
798                         tmpx += singleWidth(par, c);
799                 }
800                 ++vc;
801         }
802
803         if ((tmpx + last_tmpx) / 2 > x) {
804                 tmpx = last_tmpx;
805                 left_side = true;
806         }
807
808         BOOST_ASSERT(vc <= end);  // This shouldn't happen.
809
810         boundary = false;
811         // This (rtl_support test) is not needed, but gives
812         // some speedup if rtl_support == false
813         bool const lastrow = lyxrc.rtl_support && row.endpos() == par.size();
814
815         // If lastrow is false, we don't need to compute
816         // the value of rtl.
817         bool const rtl = lastrow ? isRTL(par) : false;
818         if (lastrow &&
819                  ((rtl  &&  left_side && vc == row.pos() && x < tmpx - 5) ||
820                   (!rtl && !left_side && vc == end  && x > tmpx + 5)))
821                 c = end;
822         else if (vc == row.pos()) {
823                 c = bidi.vis2log(vc);
824                 if (bidi.level(c) % 2 == 1)
825                         ++c;
826         } else {
827                 c = bidi.vis2log(vc - 1);
828                 bool const rtl = (bidi.level(c) % 2 == 1);
829                 if (left_side == rtl) {
830                         ++c;
831                         boundary = bidi.isBoundary(*bv()->buffer(), par, c);
832                 }
833         }
834
835 // I believe this code is not needed anymore (Jug 20050717)
836 #if 0
837         // The following code is necessary because the cursor position past
838         // the last char in a row is logically equivalent to that before
839         // the first char in the next row. That's why insets causing row
840         // divisions -- Newline and display-style insets -- must be treated
841         // specially, so cursor up/down doesn't get stuck in an air gap -- MV
842         // Newline inset, air gap below:
843         if (row.pos() < end && c >= end && par.isNewline(end - 1)) {
844                 if (bidi.level(end -1) % 2 == 0)
845                         tmpx -= singleWidth(par, end - 1);
846                 else
847                         tmpx += singleWidth(par, end - 1);
848                 c = end - 1;
849         }
850
851         // Air gap above display inset:
852         if (row.pos() < end && c >= end && end < par.size()
853             && par.isInset(end) && par.getInset(end)->display()) {
854                 c = end - 1;
855         }
856         // Air gap below display inset:
857         if (row.pos() < end && c >= end && par.isInset(end - 1)
858             && par.getInset(end - 1)->display()) {
859                 c = end - 1;
860         }
861 #endif
862
863         x = int(tmpx) + xo;
864         pos_type const col = c - row.pos();
865
866         if (!c || end == par.size())
867                 return col;
868
869         if (c==end && !par.isLineSeparator(c-1) && !par.isNewline(c-1)) {
870                 boundary = true;
871                 return col;
872         }
873
874         return min(col, end - 1 - row.pos());
875 }
876
877
878 // y is screen coordinate
879 pit_type LyXText::getPitNearY(int y) const
880 {
881         BOOST_ASSERT(!paragraphs().empty());
882         BOOST_ASSERT(theCoords.getParPos().find(this) != theCoords.getParPos().end());
883         CoordCache::InnerParPosCache const & cc = theCoords.getParPos().find(this)->second;
884         lyxerr[Debug::DEBUG]
885                 << BOOST_CURRENT_FUNCTION
886                 << ": y: " << y << " cache size: " << cc.size()
887                 << endl;
888
889         // look for highest numbered paragraph with y coordinate less than given y
890         pit_type pit = 0;
891         int yy = -1;
892         CoordCache::InnerParPosCache::const_iterator it = cc.begin();
893         CoordCache::InnerParPosCache::const_iterator et = cc.end();
894         for (; it != et; ++it) {
895                 lyxerr[Debug::DEBUG]
896                         << BOOST_CURRENT_FUNCTION
897                         << "  examining: pit: " << it->first
898                         << " y: " << it->second.y_
899                         << endl;
900                 
901                 if (it->first >= pit && int(it->second.y_) - int(pars_[it->first].ascent()) <= y) {
902                         pit = it->first;
903                         yy = it->second.y_;
904                 }
905         }
906
907         lyxerr[Debug::DEBUG]
908                 << BOOST_CURRENT_FUNCTION
909                 << ": found best y: " << yy << " for pit: " << pit
910                 << endl;
911         
912         return pit;
913 }
914
915
916 Row const & LyXText::getRowNearY(int y, pit_type pit) const
917 {
918         Paragraph const & par = pars_[pit];
919         int yy = theCoords.get(this, pit).y_ - par.ascent();
920         BOOST_ASSERT(!par.rows().empty());
921         RowList::const_iterator rit = par.rows().begin();
922         RowList::const_iterator const rlast = boost::prior(par.rows().end());
923         for (; rit != rlast; yy += rit->height(), ++rit)
924                 if (yy + rit->height() > y)
925                         break;
926         return *rit;
927 }
928
929
930 // x,y are absolute screen coordinates
931 // sets cursor recursively descending into nested editable insets
932 InsetBase * LyXText::editXY(LCursor & cur, int x, int y)
933 {
934         pit_type pit = getPitNearY(y);
935         BOOST_ASSERT(pit != -1);
936         Row const & row = getRowNearY(y, pit);
937         bool bound = false;
938
939         int xx = x; // is modified by getColumnNearX
940         pos_type const pos = row.pos() + getColumnNearX(pit, row, xx, bound);
941         cur.pit() = pit;
942         cur.pos() = pos;
943         cur.boundary(bound);
944         cur.x_target() = x;
945
946         // try to descend into nested insets
947         InsetBase * inset = checkInsetHit(x, y);
948         //lyxerr << "inset " << inset << " hit at x: " << x << " y: " << y << endl;
949         if (!inset) {
950                 // Either we deconst editXY or better we move current_font
951                 // and real_current_font to LCursor
952                 setCurrentFont(cur);
953                 return 0;
954         }
955
956         // This should be just before or just behind the
957         // cursor position set above.
958         BOOST_ASSERT((pos != 0 && inset == pars_[pit].getInset(pos - 1))
959                      || inset == pars_[pit].getInset(pos));
960         // Make sure the cursor points to the position before
961         // this inset.
962         if (inset == pars_[pit].getInset(pos - 1))
963                 --cur.pos();
964         inset = inset->editXY(cur, x, y);
965         if (cur.top().text() == this)
966                 setCurrentFont(cur);
967         return inset;
968 }
969
970
971 bool LyXText::checkAndActivateInset(LCursor & cur, bool front)
972 {
973         if (cur.selection())
974                 return false;
975         if (cur.pos() == cur.lastpos())
976                 return false;
977         InsetBase * inset = cur.nextInset();
978         if (!isHighlyEditableInset(inset))
979                 return false;
980         inset->edit(cur, front);
981         return true;
982 }
983
984
985 bool LyXText::cursorLeft(LCursor & cur)
986 {
987         if (!cur.boundary() && cur.pos() > 0 &&
988             cur.textRow().pos() == cur.pos() &&
989             !cur.paragraph().isLineSeparator(cur.pos()-1) &&
990             !cur.paragraph().isNewline(cur.pos()-1))
991         {
992                 return setCursor(cur, cur.pit(), cur.pos(), true, true);
993         }
994         if (cur.pos() != 0) {
995                 bool boundary = cur.boundary();
996                 bool updateNeeded = setCursor(cur, cur.pit(), cur.pos() - 1, true, false);
997                 if (!checkAndActivateInset(cur, false)) {
998                         if (false && !boundary &&
999                                         bidi.isBoundary(cur.buffer(), cur.paragraph(), cur.pos() + 1))
1000                                 updateNeeded |=
1001                                         setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
1002                 }
1003                 return updateNeeded;
1004         }
1005
1006         if (cur.pit() != 0) {
1007                 // Steps into the paragraph above
1008                 return setCursor(cur, cur.pit() - 1, getPar(cur.pit() - 1).size());
1009         }
1010         return false;
1011 }
1012
1013
1014 bool LyXText::cursorRight(LCursor & cur)
1015 {
1016         if (cur.boundary()) {
1017                 return setCursor(cur, cur.pit(), cur.pos(), true, false);
1018         }
1019
1020         if (cur.pos() != cur.lastpos()) {
1021                 bool updateNeeded = false;
1022                 if (!checkAndActivateInset(cur, true)) {
1023                         if (cur.textRow().endpos() == (cur.pos() + 1) &&
1024                             !cur.paragraph().isLineSeparator(cur.pos()) &&
1025                             !cur.paragraph().isNewline(cur.pos()))
1026                         {
1027                                 cur.boundary(true);
1028                         }
1029                         updateNeeded |= setCursor(cur, cur.pit(), cur.pos() + 1, true, cur.boundary());
1030                         if (false && bidi.isBoundary(cur.buffer(), cur.paragraph(),
1031                                                          cur.pos()))
1032                                 updateNeeded |= setCursor(cur, cur.pit(), cur.pos(), true, true);
1033                 }
1034                 return updateNeeded;
1035         }
1036
1037         if (cur.pit() != cur.lastpit())
1038                 return setCursor(cur, cur.pit() + 1, 0);
1039         return false;
1040 }
1041
1042
1043 bool LyXText::cursorUp(LCursor & cur)
1044 {
1045         Paragraph const & par = cur.paragraph();
1046         int row;
1047         int const x = cur.targetX();
1048
1049         if (cur.pos() && cur.boundary())
1050                 row = par.pos2row(cur.pos()-1);
1051         else
1052                 row = par.pos2row(cur.pos());
1053
1054         if (!cur.selection()) {
1055                 int const y = bv_funcs::getPos(cur, cur.boundary()).y_;
1056                 LCursor old = cur;
1057                 editXY(cur, x, y - par.rows()[row].ascent() - 1);
1058
1059                 // This happens when you move out of an inset.
1060                 // And to give the DEPM the possibility of doing
1061                 // something we must provide it with two different
1062                 // cursors. (Lgb)
1063                 LCursor dummy = cur;
1064                 if (dummy == old)
1065                         ++dummy.pos();
1066
1067                 return deleteEmptyParagraphMechanism(dummy, old);
1068         }
1069
1070         bool updateNeeded = false;
1071
1072         if (row > 0) {
1073                 updateNeeded |= setCursor(cur, cur.pit(),
1074                                           x2pos(cur.pit(), row - 1, x));
1075         } else if (cur.pit() > 0) {
1076                 --cur.pit();
1077                 //cannot use 'par' now
1078                 updateNeeded |= setCursor(cur, cur.pit(),
1079       x2pos(cur.pit(), cur.paragraph().rows().size() - 1, x));
1080         }
1081
1082         cur.x_target() = x;
1083
1084         return updateNeeded;
1085 }
1086
1087
1088 bool LyXText::cursorDown(LCursor & cur)
1089 {
1090         Paragraph const & par = cur.paragraph();
1091         int row;
1092         int const x = cur.targetX();
1093
1094         if (cur.pos() && cur.boundary())
1095                 row = par.pos2row(cur.pos()-1);
1096         else
1097                 row = par.pos2row(cur.pos());
1098
1099         if (!cur.selection()) {
1100                 int const y = bv_funcs::getPos(cur, cur.boundary()).y_;
1101                 LCursor old = cur;
1102                 editXY(cur, x, y + par.rows()[row].descent() + 1);
1103
1104                 // This happens when you move out of an inset.
1105                 // And to give the DEPM the possibility of doing
1106                 // something we must provide it with two different
1107                 // cursors. (Lgb)
1108                 LCursor dummy = cur;
1109                 if (dummy == old)
1110                         ++dummy.pos();
1111
1112                 bool const changed = deleteEmptyParagraphMechanism(dummy, old);
1113
1114                 // Make sure that cur gets back whatever happened to dummy(Lgb)
1115                 if (changed)
1116                         cur = dummy;
1117
1118                 return changed;
1119
1120         }
1121
1122         bool updateNeeded = false;
1123
1124         if (row + 1 < int(par.rows().size())) {
1125                 updateNeeded |= setCursor(cur, cur.pit(),
1126                                           x2pos(cur.pit(), row + 1, x));
1127         } else if (cur.pit() + 1 < int(paragraphs().size())) {
1128                 ++cur.pit();
1129                 updateNeeded |= setCursor(cur, cur.pit(),
1130                                           x2pos(cur.pit(), 0, x));
1131         }
1132
1133         cur.x_target() = x;
1134
1135         return updateNeeded;
1136 }
1137
1138
1139 bool LyXText::cursorUpParagraph(LCursor & cur)
1140 {
1141         bool updated = false;
1142         if (cur.pos() > 0)
1143                 updated = setCursor(cur, cur.pit(), 0);
1144         else if (cur.pit() != 0)
1145                 updated = setCursor(cur, cur.pit() - 1, 0);
1146         return updated;
1147 }
1148
1149
1150 bool LyXText::cursorDownParagraph(LCursor & cur)
1151 {
1152         bool updated = false;
1153         if (cur.pit() != cur.lastpit())
1154                 updated = setCursor(cur, cur.pit() + 1, 0);
1155         else
1156                 updated = setCursor(cur, cur.pit(), cur.lastpos());
1157         return updated;
1158 }
1159
1160
1161 // fix the cursor `cur' after a characters has been deleted at `where'
1162 // position. Called by deleteEmptyParagraphMechanism
1163 void LyXText::fixCursorAfterDelete(CursorSlice & cur, CursorSlice const & where)
1164 {
1165         // Do nothing if cursor is not in the paragraph where the
1166         // deletion occured,
1167         if (cur.pit() != where.pit())
1168                 return;
1169
1170         // If cursor position is after the deletion place update it
1171         if (cur.pos() > where.pos())
1172                 --cur.pos();
1173
1174         // Check also if we don't want to set the cursor on a spot behind the
1175         // pagragraph because we erased the last character.
1176         if (cur.pos() > cur.lastpos())
1177                 cur.pos() = cur.lastpos();
1178 }
1179
1180
1181 bool LyXText::deleteEmptyParagraphMechanism(LCursor & cur, LCursor const & old)
1182 {
1183         // Would be wrong to delete anything if we have a selection.
1184         if (cur.selection())
1185                 return false;
1186
1187         //lyxerr[Debug::DEBUG] << "DEPM: cur:\n" << cur << "old:\n" << old << endl;
1188         Paragraph const & oldpar = pars_[old.pit()];
1189
1190         // We allow all kinds of "mumbo-jumbo" when freespacing.
1191         if (oldpar.isFreeSpacing())
1192                 return false;
1193
1194         /* Ok I'll put some comments here about what is missing.
1195            I have fixed BackSpace (and thus Delete) to not delete
1196            double-spaces automagically. I have also changed Cut,
1197            Copy and Paste to hopefully do some sensible things.
1198            There are still some small problems that can lead to
1199            double spaces stored in the document file or space at
1200            the beginning of paragraphs(). This happens if you have
1201            the cursor between to spaces and then save. Or if you
1202            cut and paste and the selection have a space at the
1203            beginning and then save right after the paste. I am
1204            sure none of these are very hard to fix, but I will
1205            put out 1.1.4pre2 with FIX_DOUBLE_SPACE defined so
1206            that I can get some feedback. (Lgb)
1207         */
1208
1209         // If old.pos() == 0 and old.pos()(1) == LineSeparator
1210         // delete the LineSeparator.
1211         // MISSING
1212
1213         // If old.pos() == 1 and old.pos()(0) == LineSeparator
1214         // delete the LineSeparator.
1215         // MISSING
1216
1217         // If the chars around the old cursor were spaces, delete one of them.
1218         if (old.pit() != cur.pit() || old.pos() != cur.pos()) {
1219
1220                 // Only if the cursor has really moved.
1221                 if (old.pos() > 0
1222                     && old.pos() < oldpar.size()
1223                     && oldpar.isLineSeparator(old.pos())
1224                     && oldpar.isLineSeparator(old.pos() - 1)
1225                     && oldpar.lookupChange(old.pos() - 1) != Change::DELETED) {
1226                         // We need to set the text to Change::INSERTED to
1227                         // get it erased properly
1228                         pars_[old.pit()].setChange(old.pos() -1,
1229                                 Change::INSERTED);
1230                         pars_[old.pit()].erase(old.pos() - 1);
1231 #ifdef WITH_WARNINGS
1232 #warning This will not work anymore when we have multiple views of the same buffer
1233 // In this case, we will have to correct also the cursors held by
1234 // other bufferviews. It will probably be easier to do that in a more
1235 // automated way in CursorSlice code. (JMarc 26/09/2001)
1236 #endif
1237                         // correct all cursor parts
1238                         fixCursorAfterDelete(cur.top(), old.top());
1239 #ifdef WITH_WARNINGS
1240 #warning DEPM, look here
1241 #endif
1242                         //fixCursorAfterDelete(cur.anchor(), old.top());
1243                         return true;
1244                 }
1245         }
1246
1247         // only do our magic if we changed paragraph
1248         if (old.pit() == cur.pit())
1249                 return false;
1250
1251         // don't delete anything if this is the ONLY paragraph!
1252         if (pars_.size() == 1)
1253                 return false;
1254
1255         // Do not delete empty paragraphs with keepempty set.
1256         if (oldpar.allowEmpty())
1257                 return false;
1258
1259         // record if we have deleted a paragraph
1260         // we can't possibly have deleted a paragraph before this point
1261         bool deleted = false;
1262
1263         if (oldpar.empty() || (oldpar.size() == 1 && oldpar.isLineSeparator(0))) {
1264                 // ok, we will delete something
1265                 deleted = true;
1266
1267                 bool selection_position_was_oldcursor_position =
1268                         cur.anchor().pit() == old.pit() && cur.anchor().pos() == old.pos();
1269
1270                 // This is a bit of a overkill. We change the old and the cur par
1271                 // at max, certainly not everything in between...
1272                 recUndo(old.pit(), cur.pit());
1273
1274                 // Delete old par.
1275                 pars_.erase(pars_.begin() + old.pit());
1276
1277                 // Update cursor par offset if necessary.
1278                 // Some 'iterator registration' would be nice that takes care of
1279                 // such events. Maybe even signal/slot?
1280                 if (cur.pit() > old.pit())
1281                         --cur.pit();
1282 #ifdef WITH_WARNINGS
1283 #warning DEPM, look here
1284 #endif
1285 //              if (cur.anchor().pit() > old.pit())
1286 //                      --cur.anchor().pit();
1287
1288                 if (selection_position_was_oldcursor_position) {
1289                         // correct selection
1290                         cur.resetAnchor();
1291                 }
1292         }
1293
1294         if (deleted) {
1295                 updateCounters(cur.buffer());
1296                 return true;
1297         }
1298
1299         if (pars_[old.pit()].stripLeadingSpaces())
1300                 cur.resetAnchor();
1301
1302         return false;
1303 }
1304
1305
1306 void LyXText::recUndo(pit_type first, pit_type last) const
1307 {
1308         recordUndo(bv()->cursor(), Undo::ATOMIC, first, last);
1309 }
1310
1311
1312 void LyXText::recUndo(pit_type par) const
1313 {
1314         recordUndo(bv()->cursor(), Undo::ATOMIC, par, par);
1315 }
1316
1317
1318 int defaultRowHeight()
1319 {
1320         return int(font_metrics::maxHeight(LyXFont(LyXFont::ALL_SANE)) *  1.2);
1321 }