3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
6 * \author Asger Alstrup
7 * \author Lars Gullik Bjønnes
8 * \author Alfredo Braunstein
9 * \author Jean-Marc Lasgouttes
10 * \author Angus Leeming
12 * \author André Pönitz
15 * \author Jürgen Vigna
17 * Full author contact details are available in file CREDITS.
25 #include "buffer_funcs.h"
26 #include "bufferparams.h"
27 #include "BufferView.h"
29 #include "coordcache.h"
31 #include "CutAndPaste.h"
33 #include "dispatchresult.h"
34 #include "errorlist.h"
35 #include "funcrequest.h"
41 #include "lyxrow_funcs.h"
42 #include "paragraph.h"
43 #include "paragraph_funcs.h"
44 #include "ParagraphParameters.h"
45 #include "pariterator.h"
49 #include "frontends/font_metrics.h"
50 #include "frontends/LyXView.h"
52 #include "insets/insetenv.h"
54 #include "support/textutils.h"
56 #include <boost/current_function.hpp>
65 using std::ostringstream;
71 LyXText::LyXText(BufferView * bv)
72 : maxwidth_(bv ? bv->workWidth() : 100),
73 current_font(LyXFont::ALL_INHERIT),
74 background_color_(LColor::background),
80 void LyXText::init(BufferView * bv)
84 maxwidth_ = bv->workWidth();
89 pit_type const end = paragraphs().size();
90 for (pit_type pit = 0; pit != end; ++pit)
91 pars_[pit].rows().clear();
93 updateLabels(*bv->buffer());
97 bool LyXText::isMainText() const
99 return &bv()->buffer()->text() == this;
103 //takes screen x,y coordinates
104 InsetBase * LyXText::checkInsetHit(int x, int y) const
106 pit_type pit = getPitNearY(y);
107 BOOST_ASSERT(pit != -1);
109 Paragraph const & par = pars_[pit];
112 << BOOST_CURRENT_FUNCTION
117 InsetList::const_iterator iit = par.insetlist.begin();
118 InsetList::const_iterator iend = par.insetlist.end();
119 for (; iit != iend; ++iit) {
120 InsetBase * inset = iit->inset;
123 << BOOST_CURRENT_FUNCTION
124 << ": examining inset " << inset << endl;
126 if (theCoords.getInsets().has(inset))
128 << BOOST_CURRENT_FUNCTION
129 << ": xo: " << inset->xo() << "..."
130 << inset->xo() + inset->width()
131 << " yo: " << inset->yo() - inset->ascent()
133 << inset->yo() + inset->descent()
137 << BOOST_CURRENT_FUNCTION
138 << ": inset has no cached position" << endl;
140 if (inset->covers(x, y)) {
142 << BOOST_CURRENT_FUNCTION
143 << ": Hit inset: " << inset << endl;
148 << BOOST_CURRENT_FUNCTION
149 << ": No inset hit. " << endl;
155 // Gets the fully instantiated font at a given position in a paragraph
156 // Basically the same routine as Paragraph::getFont() in paragraph.C.
157 // The difference is that this one is used for displaying, and thus we
158 // are allowed to make cosmetic improvements. For instance make footnotes
160 LyXFont LyXText::getFont(Paragraph const & par, pos_type const pos) const
162 BOOST_ASSERT(pos >= 0);
164 LyXLayout_ptr const & layout = par.layout();
168 BufferParams const & params = bv()->buffer()->params();
169 pos_type const body_pos = par.beginOfBody();
171 // We specialize the 95% common case:
172 if (!par.getDepth()) {
173 LyXFont f = par.getFontSettings(params, pos);
178 if (layout->labeltype == LABEL_MANUAL && pos < body_pos) {
179 lf = layout->labelfont;
180 rlf = layout->reslabelfont;
183 rlf = layout->resfont;
185 // In case the default family has been customized
186 if (lf.family() == LyXFont::INHERIT_FAMILY)
187 rlf.setFamily(params.getFont().family());
188 return f.realize(rlf);
191 // The uncommon case need not be optimized as much
194 layoutfont = layout->labelfont;
196 layoutfont = layout->font;
198 LyXFont font = par.getFontSettings(params, pos);
199 font.realize(layoutfont);
202 applyOuterFont(font);
204 // Find the pit value belonging to paragraph. This will not break
205 // even if pars_ would not be a vector anymore.
206 // Performance appears acceptable.
208 pit_type pit = pars_.size();
209 for (pit_type it = 0; it < pit; ++it)
210 if (&pars_[it] == &par) {
214 // Realize against environment font information
215 // NOTE: the cast to pit_type should be removed when pit_type
216 // changes to a unsigned integer.
217 if (pit < pit_type(pars_.size()))
218 font.realize(outerFont(pit, pars_));
220 // Realize with the fonts of lesser depth.
221 font.realize(params.getFont());
226 // There are currently two font mechanisms in LyX:
227 // 1. The font attributes in a lyxtext, and
228 // 2. The inset-specific font properties, defined in an inset's
229 // metrics() and draw() methods and handed down the inset chain through
230 // the pi/mi parameters, and stored locally in a lyxtext in font_.
231 // This is where the two are integrated in the final fully realized
233 void LyXText::applyOuterFont(LyXFont & font) const {
235 lf.reduce(bv()->buffer()->params().getFont());
237 lf.setLanguage(font.language());
242 LyXFont LyXText::getLayoutFont(pit_type const pit) const
244 LyXLayout_ptr const & layout = pars_[pit].layout();
246 if (!pars_[pit].getDepth()) {
247 LyXFont lf = layout->resfont;
248 // In case the default family has been customized
249 if (layout->font.family() == LyXFont::INHERIT_FAMILY)
250 lf.setFamily(bv()->buffer()->params().getFont().family());
254 LyXFont font = layout->font;
255 // Realize with the fonts of lesser depth.
256 //font.realize(outerFont(pit, paragraphs()));
257 font.realize(bv()->buffer()->params().getFont());
263 LyXFont LyXText::getLabelFont(Paragraph const & par) const
265 LyXLayout_ptr const & layout = par.layout();
267 if (!par.getDepth()) {
268 LyXFont lf = layout->reslabelfont;
269 // In case the default family has been customized
270 if (layout->labelfont.family() == LyXFont::INHERIT_FAMILY)
271 lf.setFamily(bv()->buffer()->params().getFont().family());
275 LyXFont font = layout->labelfont;
276 // Realize with the fonts of lesser depth.
277 font.realize(bv()->buffer()->params().getFont());
283 void LyXText::setCharFont(pit_type pit, pos_type pos, LyXFont const & fnt)
286 LyXLayout_ptr const & layout = pars_[pit].layout();
288 // Get concrete layout font to reduce against
291 if (pos < pars_[pit].beginOfBody())
292 layoutfont = layout->labelfont;
294 layoutfont = layout->font;
296 // Realize against environment font information
297 if (pars_[pit].getDepth()) {
299 while (!layoutfont.resolved() &&
300 tp != pit_type(paragraphs().size()) &&
301 pars_[tp].getDepth()) {
302 tp = outerHook(tp, paragraphs());
303 if (tp != pit_type(paragraphs().size()))
304 layoutfont.realize(pars_[tp].layout()->font);
308 // Inside inset, apply the inset's font attributes if any
311 layoutfont.realize(font_);
313 layoutfont.realize(bv()->buffer()->params().getFont());
315 // Now, reduce font against full layout font
316 font.reduce(layoutfont);
318 pars_[pit].setFont(pos, font);
322 // return past-the-last paragraph influenced by a layout change on pit
323 pit_type LyXText::undoSpan(pit_type pit)
325 pit_type end = paragraphs().size();
326 pit_type nextpit = pit + 1;
329 //because of parindents
330 if (!pars_[pit].getDepth())
331 return boost::next(nextpit);
332 //because of depth constrains
333 for (; nextpit != end; ++pit, ++nextpit) {
334 if (!pars_[pit].getDepth())
341 void LyXText::setLayout(pit_type start, pit_type end, string const & layout)
343 BOOST_ASSERT(start != end);
345 BufferParams const & bufparams = bv()->buffer()->params();
346 LyXLayout_ptr const & lyxlayout = bufparams.getLyXTextClass()[layout];
348 for (pit_type pit = start; pit != end; ++pit) {
349 pars_[pit].applyLayout(lyxlayout);
350 if (lyxlayout->margintype == MARGIN_MANUAL)
351 pars_[pit].setLabelWidthString(lyxlayout->labelstring());
356 // set layout over selection and make a total rebreak of those paragraphs
357 void LyXText::setLayout(LCursor & cur, string const & layout)
359 BOOST_ASSERT(this == cur.text());
360 // special handling of new environment insets
361 BufferView & bv = cur.bv();
362 BufferParams const & params = bv.buffer()->params();
363 LyXLayout_ptr const & lyxlayout = params.getLyXTextClass()[layout];
364 if (lyxlayout->is_environment) {
365 // move everything in a new environment inset
366 lyxerr[Debug::DEBUG] << "setting layout " << layout << endl;
367 bv.owner()->dispatch(FuncRequest(LFUN_LINE_BEGIN));
368 bv.owner()->dispatch(FuncRequest(LFUN_LINE_END_SELECT));
369 bv.owner()->dispatch(FuncRequest(LFUN_CUT));
370 InsetBase * inset = new InsetEnvironment(params, layout);
371 insertInset(cur, inset);
372 //inset->edit(cur, true);
373 //bv.owner()->dispatch(FuncRequest(LFUN_PASTE));
377 pit_type start = cur.selBegin().pit();
378 pit_type end = cur.selEnd().pit() + 1;
379 pit_type undopit = undoSpan(end - 1);
380 recUndo(start, undopit - 1);
381 setLayout(start, end, layout);
382 updateLabels(cur.buffer());
389 bool changeDepthAllowed(LyXText::DEPTH_CHANGE type,
390 Paragraph const & par, int max_depth)
392 if (par.layout()->labeltype == LABEL_BIBLIO)
394 int const depth = par.params().depth();
395 if (type == LyXText::INC_DEPTH && depth < max_depth)
397 if (type == LyXText::DEC_DEPTH && depth > 0)
406 bool LyXText::changeDepthAllowed(LCursor & cur, DEPTH_CHANGE type) const
408 BOOST_ASSERT(this == cur.text());
409 // this happens when selecting several cells in tabular (bug 2630)
410 if (cur.selBegin().idx() != cur.selEnd().idx())
413 pit_type const beg = cur.selBegin().pit();
414 pit_type const end = cur.selEnd().pit() + 1;
415 int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
417 for (pit_type pit = beg; pit != end; ++pit) {
418 if (::changeDepthAllowed(type, pars_[pit], max_depth))
420 max_depth = pars_[pit].getMaxDepthAfter();
426 void LyXText::changeDepth(LCursor & cur, DEPTH_CHANGE type)
428 BOOST_ASSERT(this == cur.text());
429 pit_type const beg = cur.selBegin().pit();
430 pit_type const end = cur.selEnd().pit() + 1;
431 recordUndoSelection(cur);
432 int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
434 for (pit_type pit = beg; pit != end; ++pit) {
435 Paragraph & par = pars_[pit];
436 if (::changeDepthAllowed(type, par, max_depth)) {
437 int const depth = par.params().depth();
438 if (type == INC_DEPTH)
439 par.params().depth(depth + 1);
441 par.params().depth(depth - 1);
443 max_depth = par.getMaxDepthAfter();
445 // this handles the counter labels, and also fixes up
446 // depth values for follow-on (child) paragraphs
447 updateLabels(cur.buffer());
451 // set font over selection
452 void LyXText::setFont(LCursor & cur, LyXFont const & font, bool toggleall)
454 BOOST_ASSERT(this == cur.text());
455 // if there is no selection just set the current_font
456 if (!cur.selection()) {
457 // Determine basis font
459 pit_type pit = cur.pit();
460 if (cur.pos() < pars_[pit].beginOfBody())
461 layoutfont = getLabelFont(pars_[pit]);
463 layoutfont = getLayoutFont(pit);
465 // Update current font
466 real_current_font.update(font,
467 cur.buffer().params().language,
470 // Reduce to implicit settings
471 current_font = real_current_font;
472 current_font.reduce(layoutfont);
473 // And resolve it completely
474 real_current_font.realize(layoutfont);
479 // Ok, we have a selection.
480 recordUndoSelection(cur);
482 DocIterator dit = cur.selectionBegin();
483 DocIterator ditend = cur.selectionEnd();
485 BufferParams const & params = cur.buffer().params();
487 // Don't use forwardChar here as ditend might have
488 // pos() == lastpos() and forwardChar would miss it.
489 // Can't use forwardPos either as this descends into
491 for (; dit != ditend; dit.forwardPosNoDescend()) {
492 if (dit.pos() != dit.lastpos()) {
493 LyXFont f = getFont(dit.paragraph(), dit.pos());
494 f.update(font, params.language, toggleall);
495 setCharFont(dit.pit(), dit.pos(), f);
501 // the cursor set functions have a special mechanism. When they
502 // realize you left an empty paragraph, they will delete it.
504 bool LyXText::cursorHome(LCursor & cur)
506 BOOST_ASSERT(this == cur.text());
507 Row const & row = cur.paragraph().getRow(cur.pos(),cur.boundary());
509 return setCursor(cur, cur.pit(), row.pos());
513 bool LyXText::cursorEnd(LCursor & cur)
515 BOOST_ASSERT(this == cur.text());
516 // if not on the last row of the par, put the cursor before
517 // the final space exept if I have a spanning inset or one string
518 // is so long that we force a break.
519 pos_type end = cur.textRow().endpos();
521 // empty text, end-1 is no valid position
523 bool boundary = false;
524 if (end != cur.lastpos()) {
525 if (!cur.paragraph().isLineSeparator(end-1)
526 && !cur.paragraph().isNewline(end-1))
531 return setCursor(cur, cur.pit(), end, true, boundary);
535 bool LyXText::cursorTop(LCursor & cur)
537 BOOST_ASSERT(this == cur.text());
538 return setCursor(cur, 0, 0);
542 bool LyXText::cursorBottom(LCursor & cur)
544 BOOST_ASSERT(this == cur.text());
545 return setCursor(cur, cur.lastpit(), boost::prior(paragraphs().end())->size());
549 void LyXText::toggleFree(LCursor & cur, LyXFont const & font, bool toggleall)
551 BOOST_ASSERT(this == cur.text());
552 // If the mask is completely neutral, tell user
553 if (font == LyXFont(LyXFont::ALL_IGNORE)) {
554 // Could only happen with user style
555 cur.message(_("No font change defined. "
556 "Use Character under the Layout menu to define font change."));
560 // Try implicit word selection
561 // If there is a change in the language the implicit word selection
563 CursorSlice resetCursor = cur.top();
564 bool implicitSelection =
565 font.language() == ignore_language
566 && font.number() == LyXFont::IGNORE
567 && selectWordWhenUnderCursor(cur, lyx::WHOLE_WORD_STRICT);
570 setFont(cur, font, toggleall);
572 // Implicit selections are cleared afterwards
573 // and cursor is set to the original position.
574 if (implicitSelection) {
575 cur.clearSelection();
576 cur.top() = resetCursor;
582 string LyXText::getStringToIndex(LCursor const & cur)
584 BOOST_ASSERT(this == cur.text());
587 if (cur.selection()) {
588 idxstring = cur.selectionAsString(false);
590 // Try implicit word selection. If there is a change
591 // in the language the implicit word selection is
593 LCursor tmpcur = cur;
594 selectWord(tmpcur, lyx::PREVIOUS_WORD);
596 if (!tmpcur.selection())
597 cur.message(_("Nothing to index!"));
598 else if (tmpcur.selBegin().pit() != tmpcur.selEnd().pit())
599 cur.message(_("Cannot index more than one paragraph!"));
601 idxstring = tmpcur.selectionAsString(false);
608 void LyXText::setParagraph(LCursor & cur,
609 Spacing const & spacing, LyXAlignment align,
610 string const & labelwidthstring, bool noindent)
612 BOOST_ASSERT(cur.text());
613 // make sure that the depth behind the selection are restored, too
614 pit_type undopit = undoSpan(cur.selEnd().pit());
615 recUndo(cur.selBegin().pit(), undopit - 1);
617 for (pit_type pit = cur.selBegin().pit(), end = cur.selEnd().pit();
619 Paragraph & par = pars_[pit];
620 ParagraphParameters & params = par.params();
621 params.spacing(spacing);
623 // does the layout allow the new alignment?
624 LyXLayout_ptr const & layout = par.layout();
626 if (align == LYX_ALIGN_LAYOUT)
627 align = layout->align;
628 if (align & layout->alignpossible) {
629 if (align == layout->align)
630 params.align(LYX_ALIGN_LAYOUT);
634 par.setLabelWidthString(labelwidthstring);
635 params.noindent(noindent);
640 // this really should just insert the inset and not move the cursor.
641 void LyXText::insertInset(LCursor & cur, InsetBase * inset)
643 BOOST_ASSERT(this == cur.text());
645 cur.paragraph().insertInset(cur.pos(), inset);
649 // needed to insert the selection
650 void LyXText::insertStringAsLines(LCursor & cur, string const & str)
652 cur.buffer().insertStringAsLines(pars_, cur.pit(), cur.pos(),
653 current_font, str, autoBreakRows_);
657 // turn double CR to single CR, others are converted into one
658 // blank. Then insertStringAsLines is called
659 void LyXText::insertStringAsParagraphs(LCursor & cur, string const & str)
661 string linestr = str;
662 bool newline_inserted = false;
664 for (string::size_type i = 0, siz = linestr.size(); i < siz; ++i) {
665 if (linestr[i] == '\n') {
666 if (newline_inserted) {
667 // we know that \r will be ignored by
668 // insertStringAsLines. Of course, it is a dirty
669 // trick, but it works...
670 linestr[i - 1] = '\r';
674 newline_inserted = true;
676 } else if (isPrintable(linestr[i])) {
677 newline_inserted = false;
680 insertStringAsLines(cur, linestr);
684 bool LyXText::setCursor(LCursor & cur, pit_type par, pos_type pos,
685 bool setfont, bool boundary)
688 setCursorIntern(cur, par, pos, setfont, boundary);
689 return deleteEmptyParagraphMechanism(cur, old);
693 void LyXText::setCursor(CursorSlice & cur, pit_type par, pos_type pos)
695 BOOST_ASSERT(par != int(paragraphs().size()));
699 // now some strict checking
700 Paragraph & para = getPar(par);
702 // None of these should happen, but we're scaredy-cats
704 lyxerr << "dont like -1" << endl;
708 if (pos > para.size()) {
709 lyxerr << "dont like 1, pos: " << pos
710 << " size: " << para.size()
711 << " par: " << par << endl;
717 void LyXText::setCursorIntern(LCursor & cur,
718 pit_type par, pos_type pos, bool setfont, bool boundary)
720 cur.boundary(boundary);
721 setCursor(cur.top(), par, pos);
728 void LyXText::setCurrentFont(LCursor & cur)
730 BOOST_ASSERT(this == cur.text());
731 pos_type pos = cur.pos();
732 Paragraph & par = cur.paragraph();
734 if (cur.boundary() && pos > 0)
738 if (pos == cur.lastpos())
740 else // potentional bug... BUG (Lgb)
741 if (par.isSeparator(pos)) {
742 if (pos > cur.textRow().pos() &&
743 bidi.level(pos) % 2 ==
744 bidi.level(pos - 1) % 2)
746 else if (pos + 1 < cur.lastpos())
751 BufferParams const & bufparams = cur.buffer().params();
752 current_font = par.getFontSettings(bufparams, pos);
753 real_current_font = getFont(par, pos);
755 if (cur.pos() == cur.lastpos()
756 && bidi.isBoundary(cur.buffer(), par, cur.pos())
757 && !cur.boundary()) {
758 Language const * lang = par.getParLanguage(bufparams);
759 current_font.setLanguage(lang);
760 current_font.setNumber(LyXFont::OFF);
761 real_current_font.setLanguage(lang);
762 real_current_font.setNumber(LyXFont::OFF);
767 // x is an absolute screen coord
768 // returns the column near the specified x-coordinate of the row
769 // x is set to the real beginning of this column
770 pos_type LyXText::getColumnNearX(pit_type const pit,
771 Row const & row, int & x, bool & boundary) const
773 int const xo = theCoords.get(this, pit).x_;
775 RowMetrics const r = computeRowMetrics(pit, row);
776 Paragraph const & par = pars_[pit];
778 pos_type vc = row.pos();
779 pos_type end = row.endpos();
781 LyXLayout_ptr const & layout = par.layout();
783 bool left_side = false;
785 pos_type body_pos = par.beginOfBody();
788 double last_tmpx = tmpx;
791 (body_pos > end || !par.isLineSeparator(body_pos - 1)))
794 // check for empty row
800 while (vc < end && tmpx <= x) {
801 c = bidi.vis2log(vc);
803 if (body_pos > 0 && c == body_pos - 1) {
804 string lsep = layout->labelsep;
805 docstring dlsep(lsep.begin(), lsep.end());
806 tmpx += r.label_hfill +
807 font_metrics::width(dlsep, getLabelFont(par));
808 if (par.isLineSeparator(body_pos - 1))
809 tmpx -= singleWidth(par, body_pos - 1);
812 if (hfillExpansion(par, row, c)) {
813 tmpx += singleWidth(par, c);
817 tmpx += r.label_hfill;
818 } else if (par.isSeparator(c)) {
819 tmpx += singleWidth(par, c);
823 tmpx += singleWidth(par, c);
828 if ((tmpx + last_tmpx) / 2 > x) {
833 BOOST_ASSERT(vc <= end); // This shouldn't happen.
836 // This (rtl_support test) is not needed, but gives
837 // some speedup if rtl_support == false
838 bool const lastrow = lyxrc.rtl_support && row.endpos() == par.size();
840 // If lastrow is false, we don't need to compute
842 bool const rtl = lastrow ? isRTL(par) : false;
844 ((rtl && left_side && vc == row.pos() && x < tmpx - 5) ||
845 (!rtl && !left_side && vc == end && x > tmpx + 5)))
847 else if (vc == row.pos()) {
848 c = bidi.vis2log(vc);
849 if (bidi.level(c) % 2 == 1)
852 c = bidi.vis2log(vc - 1);
853 bool const rtl = (bidi.level(c) % 2 == 1);
854 if (left_side == rtl) {
856 boundary = bidi.isBoundary(*bv()->buffer(), par, c);
860 // I believe this code is not needed anymore (Jug 20050717)
862 // The following code is necessary because the cursor position past
863 // the last char in a row is logically equivalent to that before
864 // the first char in the next row. That's why insets causing row
865 // divisions -- Newline and display-style insets -- must be treated
866 // specially, so cursor up/down doesn't get stuck in an air gap -- MV
867 // Newline inset, air gap below:
868 if (row.pos() < end && c >= end && par.isNewline(end - 1)) {
869 if (bidi.level(end -1) % 2 == 0)
870 tmpx -= singleWidth(par, end - 1);
872 tmpx += singleWidth(par, end - 1);
876 // Air gap above display inset:
877 if (row.pos() < end && c >= end && end < par.size()
878 && par.isInset(end) && par.getInset(end)->display()) {
881 // Air gap below display inset:
882 if (row.pos() < end && c >= end && par.isInset(end - 1)
883 && par.getInset(end - 1)->display()) {
889 pos_type const col = c - row.pos();
891 if (!c || end == par.size())
894 if (c==end && !par.isLineSeparator(c-1) && !par.isNewline(c-1)) {
899 return min(col, end - 1 - row.pos());
903 // y is screen coordinate
904 pit_type LyXText::getPitNearY(int y) const
906 BOOST_ASSERT(!paragraphs().empty());
907 BOOST_ASSERT(theCoords.getParPos().find(this) != theCoords.getParPos().end());
908 CoordCache::InnerParPosCache const & cc = theCoords.getParPos().find(this)->second;
910 << BOOST_CURRENT_FUNCTION
911 << ": y: " << y << " cache size: " << cc.size()
914 // look for highest numbered paragraph with y coordinate less than given y
917 CoordCache::InnerParPosCache::const_iterator it = cc.begin();
918 CoordCache::InnerParPosCache::const_iterator et = cc.end();
919 for (; it != et; ++it) {
921 << BOOST_CURRENT_FUNCTION
922 << " examining: pit: " << it->first
923 << " y: " << it->second.y_
926 if (it->first >= pit && int(it->second.y_) - int(pars_[it->first].ascent()) <= y) {
933 << BOOST_CURRENT_FUNCTION
934 << ": found best y: " << yy << " for pit: " << pit
941 Row const & LyXText::getRowNearY(int y, pit_type pit) const
943 Paragraph const & par = pars_[pit];
944 int yy = theCoords.get(this, pit).y_ - par.ascent();
945 BOOST_ASSERT(!par.rows().empty());
946 RowList::const_iterator rit = par.rows().begin();
947 RowList::const_iterator const rlast = boost::prior(par.rows().end());
948 for (; rit != rlast; yy += rit->height(), ++rit)
949 if (yy + rit->height() > y)
955 // x,y are absolute screen coordinates
956 // sets cursor recursively descending into nested editable insets
957 InsetBase * LyXText::editXY(LCursor & cur, int x, int y)
959 pit_type pit = getPitNearY(y);
960 BOOST_ASSERT(pit != -1);
961 Row const & row = getRowNearY(y, pit);
964 int xx = x; // is modified by getColumnNearX
965 pos_type const pos = row.pos() + getColumnNearX(pit, row, xx, bound);
971 // try to descend into nested insets
972 InsetBase * inset = checkInsetHit(x, y);
973 //lyxerr << "inset " << inset << " hit at x: " << x << " y: " << y << endl;
975 // Either we deconst editXY or better we move current_font
976 // and real_current_font to LCursor
981 // This should be just before or just behind the
982 // cursor position set above.
983 BOOST_ASSERT((pos != 0 && inset == pars_[pit].getInset(pos - 1))
984 || inset == pars_[pit].getInset(pos));
985 // Make sure the cursor points to the position before
987 if (inset == pars_[pit].getInset(pos - 1))
989 inset = inset->editXY(cur, x, y);
990 if (cur.top().text() == this)
996 bool LyXText::checkAndActivateInset(LCursor & cur, bool front)
1000 if (cur.pos() == cur.lastpos())
1002 InsetBase * inset = cur.nextInset();
1003 if (!isHighlyEditableInset(inset))
1005 inset->edit(cur, front);
1010 bool LyXText::cursorLeft(LCursor & cur)
1012 if (!cur.boundary() && cur.pos() > 0 &&
1013 cur.textRow().pos() == cur.pos() &&
1014 !cur.paragraph().isLineSeparator(cur.pos()-1) &&
1015 !cur.paragraph().isNewline(cur.pos()-1)) {
1016 return setCursor(cur, cur.pit(), cur.pos(), true, true);
1018 if (cur.pos() != 0) {
1019 bool boundary = cur.boundary();
1020 bool updateNeeded = setCursor(cur, cur.pit(), cur.pos() - 1, true, false);
1021 if (!checkAndActivateInset(cur, false)) {
1022 if (false && !boundary &&
1023 bidi.isBoundary(cur.buffer(), cur.paragraph(), cur.pos() + 1))
1025 setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
1027 return updateNeeded;
1030 if (cur.pit() != 0) {
1031 // Steps into the paragraph above
1032 return setCursor(cur, cur.pit() - 1, getPar(cur.pit() - 1).size());
1038 bool LyXText::cursorRight(LCursor & cur)
1040 if (cur.pos() != cur.lastpos()) {
1042 return setCursor(cur, cur.pit(), cur.pos(),
1045 bool updateNeeded = false;
1046 if (!checkAndActivateInset(cur, true)) {
1047 if (cur.textRow().endpos() == cur.pos() + 1 &&
1048 cur.textRow().endpos() != cur.lastpos() &&
1049 !cur.paragraph().isLineSeparator(cur.pos()) &&
1050 !cur.paragraph().isNewline(cur.pos())) {
1053 updateNeeded |= setCursor(cur, cur.pit(), cur.pos() + 1, true, cur.boundary());
1054 if (false && bidi.isBoundary(cur.buffer(), cur.paragraph(),
1056 updateNeeded |= setCursor(cur, cur.pit(), cur.pos(), true, true);
1058 return updateNeeded;
1061 if (cur.pit() != cur.lastpit())
1062 return setCursor(cur, cur.pit() + 1, 0);
1067 bool LyXText::cursorUp(LCursor & cur)
1069 Paragraph const & par = cur.paragraph();
1071 int const x = cur.targetX();
1073 if (cur.pos() && cur.boundary())
1074 row = par.pos2row(cur.pos()-1);
1076 row = par.pos2row(cur.pos());
1078 if (!cur.selection()) {
1079 int const y = bv_funcs::getPos(cur, cur.boundary()).y_;
1081 editXY(cur, x, y - par.rows()[row].ascent() - 1);
1082 cur.clearSelection();
1084 // This happens when you move out of an inset.
1085 // And to give the DEPM the possibility of doing
1086 // something we must provide it with two different
1088 LCursor dummy = cur;
1092 return deleteEmptyParagraphMechanism(dummy, old);
1095 bool updateNeeded = false;
1098 updateNeeded |= setCursor(cur, cur.pit(),
1099 x2pos(cur.pit(), row - 1, x));
1100 } else if (cur.pit() > 0) {
1102 //cannot use 'par' now
1103 updateNeeded |= setCursor(cur, cur.pit(),
1104 x2pos(cur.pit(), cur.paragraph().rows().size() - 1, x));
1109 return updateNeeded;
1113 bool LyXText::cursorDown(LCursor & cur)
1115 Paragraph const & par = cur.paragraph();
1117 int const x = cur.targetX();
1119 if (cur.pos() && cur.boundary())
1120 row = par.pos2row(cur.pos()-1);
1122 row = par.pos2row(cur.pos());
1124 if (!cur.selection()) {
1125 int const y = bv_funcs::getPos(cur, cur.boundary()).y_;
1127 editXY(cur, x, y + par.rows()[row].descent() + 1);
1128 cur.clearSelection();
1130 // This happens when you move out of an inset.
1131 // And to give the DEPM the possibility of doing
1132 // something we must provide it with two different
1134 LCursor dummy = cur;
1138 bool const changed = deleteEmptyParagraphMechanism(dummy, old);
1140 // Make sure that cur gets back whatever happened to dummy(Lgb)
1147 bool updateNeeded = false;
1149 if (row + 1 < int(par.rows().size())) {
1150 updateNeeded |= setCursor(cur, cur.pit(),
1151 x2pos(cur.pit(), row + 1, x));
1152 } else if (cur.pit() + 1 < int(paragraphs().size())) {
1154 updateNeeded |= setCursor(cur, cur.pit(),
1155 x2pos(cur.pit(), 0, x));
1160 return updateNeeded;
1164 bool LyXText::cursorUpParagraph(LCursor & cur)
1166 bool updated = false;
1168 updated = setCursor(cur, cur.pit(), 0);
1169 else if (cur.pit() != 0)
1170 updated = setCursor(cur, cur.pit() - 1, 0);
1175 bool LyXText::cursorDownParagraph(LCursor & cur)
1177 bool updated = false;
1178 if (cur.pit() != cur.lastpit())
1179 updated = setCursor(cur, cur.pit() + 1, 0);
1181 updated = setCursor(cur, cur.pit(), cur.lastpos());
1186 // fix the cursor `cur' after a characters has been deleted at `where'
1187 // position. Called by deleteEmptyParagraphMechanism
1188 void LyXText::fixCursorAfterDelete(CursorSlice & cur, CursorSlice const & where)
1190 // Do nothing if cursor is not in the paragraph where the
1191 // deletion occured,
1192 if (cur.pit() != where.pit())
1195 // If cursor position is after the deletion place update it
1196 if (cur.pos() > where.pos())
1199 // Check also if we don't want to set the cursor on a spot behind the
1200 // pagragraph because we erased the last character.
1201 if (cur.pos() > cur.lastpos())
1202 cur.pos() = cur.lastpos();
1206 bool LyXText::deleteEmptyParagraphMechanism(LCursor & cur, LCursor & old)
1208 // Would be wrong to delete anything if we have a selection.
1209 if (cur.selection())
1212 //lyxerr[Debug::DEBUG] << "DEPM: cur:\n" << cur << "old:\n" << old << endl;
1213 // old should point to us
1214 BOOST_ASSERT(old.text() == this);
1216 Paragraph & oldpar = old.paragraph();
1218 // We allow all kinds of "mumbo-jumbo" when freespacing.
1219 if (oldpar.isFreeSpacing())
1222 /* Ok I'll put some comments here about what is missing.
1223 There are still some small problems that can lead to
1224 double spaces stored in the document file or space at
1225 the beginning of paragraphs(). This happens if you have
1226 the cursor between to spaces and then save. Or if you
1227 cut and paste and the selection have a space at the
1228 beginning and then save right after the paste. (Lgb)
1231 // If old.pos() == 0 and old.pos()(1) == LineSeparator
1232 // delete the LineSeparator.
1235 // If old.pos() == 1 and old.pos()(0) == LineSeparator
1236 // delete the LineSeparator.
1239 bool const same_inset = &old.inset() == &cur.inset();
1240 bool const same_par = same_inset && old.pit() == cur.pit();
1241 bool const same_par_pos = same_par && old.pos() == cur.pos();
1243 // If the chars around the old cursor were spaces, delete one of them.
1244 if (!same_par_pos) {
1245 // Only if the cursor has really moved.
1247 && old.pos() < oldpar.size()
1248 && oldpar.isLineSeparator(old.pos())
1249 && oldpar.isLineSeparator(old.pos() - 1)
1250 && oldpar.lookupChange(old.pos() - 1) != Change::DELETED) {
1251 // We need to set the text to Change::INSERTED to
1252 // get it erased properly
1253 oldpar.setChange(old.pos() -1, Change::INSERTED);
1254 oldpar.erase(old.pos() - 1);
1255 #ifdef WITH_WARNINGS
1256 #warning This will not work anymore when we have multiple views of the same buffer
1257 // In this case, we will have to correct also the cursors held by
1258 // other bufferviews. It will probably be easier to do that in a more
1259 // automated way in CursorSlice code. (JMarc 26/09/2001)
1261 // correct all cursor parts
1263 fixCursorAfterDelete(cur.top(), old.top());
1270 // only do our magic if we changed paragraph
1274 // don't delete anything if this is the ONLY paragraph!
1275 if (old.lastpit() == 0)
1278 // Do not delete empty paragraphs with keepempty set.
1279 if (oldpar.allowEmpty())
1282 if (oldpar.empty() || (oldpar.size() == 1 && oldpar.isLineSeparator(0))) {
1284 recordUndo(old, Undo::ATOMIC,
1285 max(old.pit() - 1, pit_type(0)),
1286 min(old.pit() + 1, old.lastpit()));
1287 ParagraphList & plist = old.text()->paragraphs();
1288 plist.erase(boost::next(plist.begin(), old.pit()));
1290 // see #warning above
1291 if (cur.depth() >= old.depth()) {
1292 CursorSlice & curslice = cur[old.depth() - 1];
1293 if (&curslice.inset() == &old.inset()
1294 && curslice.pit() > old.pit()) {
1296 // since a paragraph has been deleted, all the
1297 // insets after `old' have been copied and
1298 // their address has changed. Therefore we
1299 // need to `regenerate' cur. (JMarc)
1300 cur.updateInsets(&(cur.bottom().inset()));
1304 // There is a crash reported by Edwin Leuven (16/04/2006) because of:
1305 //ParIterator par_it(old);
1306 //updateLabels(old.buffer(), par_it);
1307 // So for now we do the full update:
1308 updateLabels(old.buffer());
1312 if (oldpar.stripLeadingSpaces())
1319 void LyXText::recUndo(pit_type first, pit_type last) const
1321 recordUndo(bv()->cursor(), Undo::ATOMIC, first, last);
1325 void LyXText::recUndo(pit_type par) const
1327 recordUndo(bv()->cursor(), Undo::ATOMIC, par, par);
1331 int defaultRowHeight()
1333 return int(font_metrics::maxHeight(LyXFont(LyXFont::ALL_SANE)) * 1.2);