]> git.lyx.org Git - lyx.git/blob - src/text2.C
Two fixes involving RtL text drawing
[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
1060                 // This happens when you move out of an inset.
1061                 // And to give the DEPM the possibility of doing
1062                 // something we must provide it with two different
1063                 // cursors. (Lgb)
1064                 LCursor dummy = cur;
1065                 if (dummy == old)
1066                         ++dummy.pos();
1067
1068                 return deleteEmptyParagraphMechanism(dummy, old);
1069         }
1070
1071         bool updateNeeded = false;
1072
1073         if (row > 0) {
1074                 updateNeeded |= setCursor(cur, cur.pit(),
1075                                           x2pos(cur.pit(), row - 1, x));
1076         } else if (cur.pit() > 0) {
1077                 --cur.pit();
1078                 //cannot use 'par' now
1079                 updateNeeded |= setCursor(cur, cur.pit(),
1080                                           x2pos(cur.pit(), cur.paragraph().rows().size() - 1, x));
1081         }
1082
1083         cur.x_target() = x;
1084
1085         return updateNeeded;
1086 }
1087
1088
1089 bool LyXText::cursorDown(LCursor & cur)
1090 {
1091         Paragraph const & par = cur.paragraph();
1092         int row;
1093         int const x = cur.targetX();
1094
1095         if (cur.pos() && cur.boundary())
1096                 row = par.pos2row(cur.pos()-1);
1097         else
1098                 row = par.pos2row(cur.pos());
1099
1100         if (!cur.selection()) {
1101                 int const y = bv_funcs::getPos(cur, cur.boundary()).y_;
1102                 LCursor old = cur;
1103                 editXY(cur, x, y + par.rows()[row].descent() + 1);
1104
1105                 // This happens when you move out of an inset.
1106                 // And to give the DEPM the possibility of doing
1107                 // something we must provide it with two different
1108                 // cursors. (Lgb)
1109                 LCursor dummy = cur;
1110                 if (dummy == old)
1111                         ++dummy.pos();
1112
1113                 bool const changed = deleteEmptyParagraphMechanism(dummy, old);
1114
1115                 // Make sure that cur gets back whatever happened to dummy(Lgb)
1116                 if (changed)
1117                         cur = dummy;
1118
1119                 return changed;
1120
1121         }
1122
1123         bool updateNeeded = false;
1124
1125         if (row + 1 < int(par.rows().size())) {
1126                 updateNeeded |= setCursor(cur, cur.pit(),
1127                                           x2pos(cur.pit(), row + 1, x));
1128         } else if (cur.pit() + 1 < int(paragraphs().size())) {
1129                 ++cur.pit();
1130                 updateNeeded |= setCursor(cur, cur.pit(),
1131                                           x2pos(cur.pit(), 0, x));
1132         }
1133
1134         cur.x_target() = x;
1135
1136         return updateNeeded;
1137 }
1138
1139
1140 bool LyXText::cursorUpParagraph(LCursor & cur)
1141 {
1142         bool updated = false;
1143         if (cur.pos() > 0)
1144                 updated = setCursor(cur, cur.pit(), 0);
1145         else if (cur.pit() != 0)
1146                 updated = setCursor(cur, cur.pit() - 1, 0);
1147         return updated;
1148 }
1149
1150
1151 bool LyXText::cursorDownParagraph(LCursor & cur)
1152 {
1153         bool updated = false;
1154         if (cur.pit() != cur.lastpit())
1155                 updated = setCursor(cur, cur.pit() + 1, 0);
1156         else
1157                 updated = setCursor(cur, cur.pit(), cur.lastpos());
1158         return updated;
1159 }
1160
1161
1162 // fix the cursor `cur' after a characters has been deleted at `where'
1163 // position. Called by deleteEmptyParagraphMechanism
1164 void LyXText::fixCursorAfterDelete(CursorSlice & cur, CursorSlice const & where)
1165 {
1166         // Do nothing if cursor is not in the paragraph where the
1167         // deletion occured,
1168         if (cur.pit() != where.pit())
1169                 return;
1170
1171         // If cursor position is after the deletion place update it
1172         if (cur.pos() > where.pos())
1173                 --cur.pos();
1174
1175         // Check also if we don't want to set the cursor on a spot behind the
1176         // pagragraph because we erased the last character.
1177         if (cur.pos() > cur.lastpos())
1178                 cur.pos() = cur.lastpos();
1179 }
1180
1181
1182 bool LyXText::deleteEmptyParagraphMechanism(LCursor & cur, LCursor const & old)
1183 {
1184         // Would be wrong to delete anything if we have a selection.
1185         if (cur.selection())
1186                 return false;
1187
1188         //lyxerr[Debug::DEBUG] << "DEPM: cur:\n" << cur << "old:\n" << old << endl;
1189         Paragraph const & oldpar = pars_[old.pit()];
1190
1191         // We allow all kinds of "mumbo-jumbo" when freespacing.
1192         if (oldpar.isFreeSpacing())
1193                 return false;
1194
1195         /* Ok I'll put some comments here about what is missing.
1196            I have fixed BackSpace (and thus Delete) to not delete
1197            double-spaces automagically. I have also changed Cut,
1198            Copy and Paste to hopefully do some sensible things.
1199            There are still some small problems that can lead to
1200            double spaces stored in the document file or space at
1201            the beginning of paragraphs(). This happens if you have
1202            the cursor between to spaces and then save. Or if you
1203            cut and paste and the selection have a space at the
1204            beginning and then save right after the paste. I am
1205            sure none of these are very hard to fix, but I will
1206            put out 1.1.4pre2 with FIX_DOUBLE_SPACE defined so
1207            that I can get some feedback. (Lgb)
1208         */
1209
1210         // If old.pos() == 0 and old.pos()(1) == LineSeparator
1211         // delete the LineSeparator.
1212         // MISSING
1213
1214         // If old.pos() == 1 and old.pos()(0) == LineSeparator
1215         // delete the LineSeparator.
1216         // MISSING
1217
1218         // If the chars around the old cursor were spaces, delete one of them.
1219         if (old.pit() != cur.pit() || old.pos() != cur.pos()) {
1220
1221                 // Only if the cursor has really moved.
1222                 if (old.pos() > 0
1223                     && old.pos() < oldpar.size()
1224                     && oldpar.isLineSeparator(old.pos())
1225                     && oldpar.isLineSeparator(old.pos() - 1)
1226                     && oldpar.lookupChange(old.pos() - 1) != Change::DELETED) {
1227                         // We need to set the text to Change::INSERTED to
1228                         // get it erased properly
1229                         pars_[old.pit()].setChange(old.pos() -1,
1230                                                    Change::INSERTED);
1231                         pars_[old.pit()].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                         fixCursorAfterDelete(cur.top(), old.top());
1240 #ifdef WITH_WARNINGS
1241 #warning DEPM, look here
1242 #endif
1243                         //fixCursorAfterDelete(cur.anchor(), old.top());
1244                         return true;
1245                 }
1246         }
1247
1248         // only do our magic if we changed paragraph
1249         if (old.pit() == cur.pit())
1250                 return false;
1251
1252         // don't delete anything if this is the ONLY paragraph!
1253         if (pars_.size() == 1)
1254                 return false;
1255
1256         // Do not delete empty paragraphs with keepempty set.
1257         if (oldpar.allowEmpty())
1258                 return false;
1259
1260         // record if we have deleted a paragraph
1261         // we can't possibly have deleted a paragraph before this point
1262         bool deleted = false;
1263
1264         if (oldpar.empty() || (oldpar.size() == 1 && oldpar.isLineSeparator(0))) {
1265                 // ok, we will delete something
1266                 deleted = true;
1267
1268                 bool selection_position_was_oldcursor_position =
1269                         cur.anchor().pit() == old.pit() && cur.anchor().pos() == old.pos();
1270
1271                 // This is a bit of a overkill. We change the old and the cur par
1272                 // at max, certainly not everything in between...
1273                 recUndo(old.pit(), cur.pit());
1274
1275                 // Delete old par.
1276                 pars_.erase(pars_.begin() + old.pit());
1277
1278                 // Update cursor par offset if necessary.
1279                 // Some 'iterator registration' would be nice that takes care of
1280                 // such events. Maybe even signal/slot?
1281                 if (cur.pit() > old.pit())
1282                         --cur.pit();
1283 #ifdef WITH_WARNINGS
1284 #warning DEPM, look here
1285 #endif
1286 //              if (cur.anchor().pit() > old.pit())
1287 //                      --cur.anchor().pit();
1288
1289                 if (selection_position_was_oldcursor_position) {
1290                         // correct selection
1291                         cur.resetAnchor();
1292                 }
1293         }
1294
1295         if (deleted) {
1296                 updateCounters(cur.buffer());
1297                 return true;
1298         }
1299
1300         if (pars_[old.pit()].stripLeadingSpaces())
1301                 cur.resetAnchor();
1302
1303         return false;
1304 }
1305
1306
1307 void LyXText::recUndo(pit_type first, pit_type last) const
1308 {
1309         recordUndo(bv()->cursor(), Undo::ATOMIC, first, last);
1310 }
1311
1312
1313 void LyXText::recUndo(pit_type par) const
1314 {
1315         recordUndo(bv()->cursor(), Undo::ATOMIC, par, par);
1316 }
1317
1318
1319 int defaultRowHeight()
1320 {
1321         return int(font_metrics::maxHeight(LyXFont(LyXFont::ALL_SANE)) *  1.2);
1322 }