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