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 "BufferList.h"
27 #include "BufferParams.h"
28 #include "BufferView.h"
29 #include "bufferview_funcs.h"
31 #include "CoordCache.h"
33 #include "CutAndPaste.h"
35 #include "DispatchResult.h"
36 #include "ErrorList.h"
37 #include "FuncRequest.h"
44 #include "Paragraph.h"
45 #include "TextMetrics.h"
46 #include "paragraph_funcs.h"
47 #include "ParagraphParameters.h"
48 #include "ParIterator.h"
50 #include "ServerSocket.h"
54 #include "frontends/FontMetrics.h"
56 #include "insets/InsetEnvironment.h"
58 #include "mathed/InsetMathHull.h"
60 #include "support/textutils.h"
62 #include <boost/current_function.hpp>
70 using std::ostringstream;
77 : current_font(Font::ALL_INHERIT),
78 background_color_(Color::background),
83 bool Text::isMainText(Buffer const & buffer) const
85 return &buffer.text() == this;
89 //takes screen x,y coordinates
90 Inset * Text::checkInsetHit(BufferView & bv, int x, int y)
92 pit_type pit = getPitNearY(bv, y);
93 BOOST_ASSERT(pit != -1);
95 Paragraph const & par = pars_[pit];
98 << BOOST_CURRENT_FUNCTION
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;
109 << BOOST_CURRENT_FUNCTION
110 << ": examining inset " << inset << endl;
112 if (bv.coordCache().getInsets().has(inset))
114 << BOOST_CURRENT_FUNCTION
115 << ": xo: " << inset->xo(bv) << "..."
116 << inset->xo(bv) + inset->width()
117 << " yo: " << inset->yo(bv) - inset->ascent()
119 << inset->yo(bv) + inset->descent()
123 << BOOST_CURRENT_FUNCTION
124 << ": inset has no cached position" << endl;
126 if (inset->covers(bv, x, y)) {
128 << BOOST_CURRENT_FUNCTION
129 << ": Hit inset: " << inset << endl;
134 << BOOST_CURRENT_FUNCTION
135 << ": No inset hit. " << endl;
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
146 Font Text::getFont(Buffer const & buffer, Paragraph const & par,
147 pos_type const pos) const
149 BOOST_ASSERT(pos >= 0);
151 Layout_ptr const & layout = par.layout();
155 BufferParams const & params = buffer.params();
156 pos_type const body_pos = par.beginOfBody();
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);
165 if (layout->labeltype == LABEL_MANUAL && pos < body_pos) {
166 lf = layout->labelfont;
167 rlf = layout->reslabelfont;
170 rlf = layout->resfont;
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);
178 // The uncommon case need not be optimized as much
181 layoutfont = layout->labelfont;
183 layoutfont = layout->font;
185 Font font = par.getFontSettings(params, pos);
186 font.realize(layoutfont);
188 if (!isMainText(buffer))
189 applyOuterFont(buffer, font);
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.
195 pit_type pit = pars_.size();
196 for (pit_type it = 0; it < pit; ++it)
197 if (&pars_[it] == &par) {
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_));
207 // Realize with the fonts of lesser depth.
208 font.realize(params.getFont());
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
220 void Text::applyOuterFont(Buffer const & buffer, Font & font) const {
222 lf.reduce(buffer.params().getFont());
224 lf.setLanguage(font.language());
229 Font Text::getLayoutFont(Buffer const & buffer, pit_type const pit) const
231 Layout_ptr const & layout = pars_[pit].layout();
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());
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());
250 Font Text::getLabelFont(Buffer const & buffer, Paragraph const & par) const
252 Layout_ptr const & layout = par.layout();
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());
262 Font font = layout->labelfont;
263 // Realize with the fonts of lesser depth.
264 font.realize(buffer.params().getFont());
270 void Text::setCharFont(Buffer const & buffer, pit_type pit,
271 pos_type pos, Font const & fnt)
274 Layout_ptr const & layout = pars_[pit].layout();
276 // Get concrete layout font to reduce against
279 if (pos < pars_[pit].beginOfBody())
280 layoutfont = layout->labelfont;
282 layoutfont = layout->font;
284 // Realize against environment font information
285 if (pars_[pit].getDepth()) {
287 while (!layoutfont.resolved() &&
288 tp != pit_type(paragraphs().size()) &&
289 pars_[tp].getDepth()) {
290 tp = outerHook(tp, paragraphs());
291 if (tp != pit_type(paragraphs().size()))
292 layoutfont.realize(pars_[tp].layout()->font);
296 // Inside inset, apply the inset's font attributes if any
298 if (!isMainText(buffer))
299 layoutfont.realize(font_);
301 layoutfont.realize(buffer.params().getFont());
303 // Now, reduce font against full layout font
304 font.reduce(layoutfont);
306 pars_[pit].setFont(pos, font);
310 void Text::setInsetFont(Buffer const & buffer, pit_type pit,
311 pos_type pos, Font const & font, bool toggleall)
313 BOOST_ASSERT(pars_[pit].isInset(pos) &&
314 pars_[pit].getInset(pos)->noFontChange());
316 Inset * const inset = pars_[pit].getInset(pos);
317 DocIterator dit = doc_iterator_begin(*inset);
318 // start of the last cell
319 DocIterator end = dit;
320 end.idx() = end.lastidx();
323 Text * text = dit.text();
324 Inset * cell = dit.realInset();
326 DocIterator cellbegin = doc_iterator_begin(*cell);
327 // last position of the cell
328 DocIterator cellend = cellbegin;
329 cellend.pit() = cellend.lastpit();
330 cellend.pos() = cellend.lastpos();
331 text->setFont(buffer, cellbegin, cellend, font, toggleall);
340 // return past-the-last paragraph influenced by a layout change on pit
341 pit_type Text::undoSpan(pit_type pit)
343 pit_type end = paragraphs().size();
344 pit_type nextpit = pit + 1;
347 //because of parindents
348 if (!pars_[pit].getDepth())
349 return boost::next(nextpit);
350 //because of depth constrains
351 for (; nextpit != end; ++pit, ++nextpit) {
352 if (!pars_[pit].getDepth())
359 void Text::setLayout(Buffer const & buffer, pit_type start, pit_type end,
360 string const & layout)
362 BOOST_ASSERT(start != end);
364 BufferParams const & bufparams = buffer.params();
365 Layout_ptr const & lyxlayout = bufparams.getTextClass()[layout];
367 for (pit_type pit = start; pit != end; ++pit) {
368 Paragraph & par = pars_[pit];
369 par.applyLayout(lyxlayout);
370 if (lyxlayout->margintype == MARGIN_MANUAL)
371 par.setLabelWidthString(par.translateIfPossible(
372 lyxlayout->labelstring(), buffer.params()));
377 // set layout over selection and make a total rebreak of those paragraphs
378 void Text::setLayout(Cursor & cur, string const & layout)
380 BOOST_ASSERT(this == cur.text());
381 // special handling of new environment insets
382 BufferView & bv = cur.bv();
383 BufferParams const & params = bv.buffer()->params();
384 Layout_ptr const & lyxlayout = params.getTextClass()[layout];
385 if (lyxlayout->is_environment) {
386 // move everything in a new environment inset
387 LYXERR(Debug::DEBUG) << "setting layout " << layout << endl;
388 lyx::dispatch(FuncRequest(LFUN_LINE_BEGIN));
389 lyx::dispatch(FuncRequest(LFUN_LINE_END_SELECT));
390 lyx::dispatch(FuncRequest(LFUN_CUT));
391 Inset * inset = new InsetEnvironment(params, layout);
392 insertInset(cur, inset);
393 //inset->edit(cur, true);
394 //lyx::dispatch(FuncRequest(LFUN_PASTE));
398 pit_type start = cur.selBegin().pit();
399 pit_type end = cur.selEnd().pit() + 1;
400 pit_type undopit = undoSpan(end - 1);
401 recUndo(cur, start, undopit - 1);
402 setLayout(cur.buffer(), start, end, layout);
403 updateLabels(cur.buffer());
407 static bool changeDepthAllowed(Text::DEPTH_CHANGE type,
408 Paragraph const & par, int max_depth)
410 if (par.layout()->labeltype == LABEL_BIBLIO)
412 int const depth = par.params().depth();
413 if (type == Text::INC_DEPTH && depth < max_depth)
415 if (type == Text::DEC_DEPTH && depth > 0)
421 bool Text::changeDepthAllowed(Cursor & cur, DEPTH_CHANGE type) const
423 BOOST_ASSERT(this == cur.text());
424 // this happens when selecting several cells in tabular (bug 2630)
425 if (cur.selBegin().idx() != cur.selEnd().idx())
428 pit_type const beg = cur.selBegin().pit();
429 pit_type const end = cur.selEnd().pit() + 1;
430 int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
432 for (pit_type pit = beg; pit != end; ++pit) {
433 if (lyx::changeDepthAllowed(type, pars_[pit], max_depth))
435 max_depth = pars_[pit].getMaxDepthAfter();
441 void Text::changeDepth(Cursor & cur, DEPTH_CHANGE type)
443 BOOST_ASSERT(this == cur.text());
444 pit_type const beg = cur.selBegin().pit();
445 pit_type const end = cur.selEnd().pit() + 1;
446 recordUndoSelection(cur);
447 int max_depth = (beg != 0 ? pars_[beg - 1].getMaxDepthAfter() : 0);
449 for (pit_type pit = beg; pit != end; ++pit) {
450 Paragraph & par = pars_[pit];
451 if (lyx::changeDepthAllowed(type, par, max_depth)) {
452 int const depth = par.params().depth();
453 if (type == INC_DEPTH)
454 par.params().depth(depth + 1);
456 par.params().depth(depth - 1);
458 max_depth = par.getMaxDepthAfter();
460 // this handles the counter labels, and also fixes up
461 // depth values for follow-on (child) paragraphs
462 updateLabels(cur.buffer());
466 void Text::setFont(Cursor & cur, Font const & font, bool toggleall)
468 BOOST_ASSERT(this == cur.text());
469 // Set the current_font
470 // Determine basis font
472 pit_type pit = cur.pit();
473 if (cur.pos() < pars_[pit].beginOfBody())
474 layoutfont = getLabelFont(cur.buffer(), pars_[pit]);
476 layoutfont = getLayoutFont(cur.buffer(), pit);
478 // Update current font
479 real_current_font.update(font,
480 cur.buffer().params().language,
483 // Reduce to implicit settings
484 current_font = real_current_font;
485 current_font.reduce(layoutfont);
486 // And resolve it completely
487 real_current_font.realize(layoutfont);
489 // if there is no selection that's all we need to do
490 if (!cur.selection())
493 // Ok, we have a selection.
494 recordUndoSelection(cur);
496 setFont(cur.buffer(), cur.selectionBegin(), cur.selectionEnd(), font,
501 void Text::setFont(Buffer const & buffer, DocIterator const & begin,
502 DocIterator const & end, Font const & font,
505 // Don't use forwardChar here as ditend might have
506 // pos() == lastpos() and forwardChar would miss it.
507 // Can't use forwardPos either as this descends into
509 Language const * language = buffer.params().language;
510 for (DocIterator dit = begin; dit != end; dit.forwardPosNoDescend()) {
511 if (dit.pos() != dit.lastpos()) {
512 pit_type const pit = dit.pit();
513 pos_type const pos = dit.pos();
514 if (pars_[pit].isInset(pos) &&
515 pars_[pit].getInset(pos)->noFontChange())
516 // We need to propagate the font change to all
517 // text cells of the inset (bug 1973).
518 // FIXME: This should change, see documentation
519 // of noFontChange in Inset.h
520 setInsetFont(buffer, pit, pos, font, toggleall);
521 Font f = getFont(buffer, dit.paragraph(), pos);
522 f.update(font, language, toggleall);
523 setCharFont(buffer, pit, pos, f);
529 // the cursor set functions have a special mechanism. When they
530 // realize you left an empty paragraph, they will delete it.
532 bool Text::cursorHome(Cursor & cur)
534 BOOST_ASSERT(this == cur.text());
535 ParagraphMetrics const & pm = cur.bv().parMetrics(this, cur.pit());
536 Row const & row = pm.getRow(cur.pos(),cur.boundary());
537 return setCursor(cur, cur.pit(), row.pos());
541 bool Text::cursorEnd(Cursor & cur)
543 BOOST_ASSERT(this == cur.text());
544 // if not on the last row of the par, put the cursor before
545 // the final space exept if I have a spanning inset or one string
546 // is so long that we force a break.
547 pos_type end = cur.textRow().endpos();
549 // empty text, end-1 is no valid position
551 bool boundary = false;
552 if (end != cur.lastpos()) {
553 if (!cur.paragraph().isLineSeparator(end-1)
554 && !cur.paragraph().isNewline(end-1))
559 return setCursor(cur, cur.pit(), end, true, boundary);
563 bool Text::cursorTop(Cursor & cur)
565 BOOST_ASSERT(this == cur.text());
566 return setCursor(cur, 0, 0);
570 bool Text::cursorBottom(Cursor & cur)
572 BOOST_ASSERT(this == cur.text());
573 return setCursor(cur, cur.lastpit(), boost::prior(paragraphs().end())->size());
577 void Text::toggleFree(Cursor & cur, Font const & font, bool toggleall)
579 BOOST_ASSERT(this == cur.text());
580 // If the mask is completely neutral, tell user
581 if (font == Font(Font::ALL_IGNORE)) {
582 // Could only happen with user style
583 cur.message(_("No font change defined."));
587 // Try implicit word selection
588 // If there is a change in the language the implicit word selection
590 CursorSlice resetCursor = cur.top();
591 bool implicitSelection =
592 font.language() == ignore_language
593 && font.number() == Font::IGNORE
594 && selectWordWhenUnderCursor(cur, WHOLE_WORD_STRICT);
597 setFont(cur, font, toggleall);
599 // Implicit selections are cleared afterwards
600 // and cursor is set to the original position.
601 if (implicitSelection) {
602 cur.clearSelection();
603 cur.top() = resetCursor;
609 docstring Text::getStringToIndex(Cursor const & cur)
611 BOOST_ASSERT(this == cur.text());
615 idxstring = cur.selectionAsString(false);
617 // Try implicit word selection. If there is a change
618 // in the language the implicit word selection is
621 selectWord(tmpcur, PREVIOUS_WORD);
623 if (!tmpcur.selection())
624 cur.message(_("Nothing to index!"));
625 else if (tmpcur.selBegin().pit() != tmpcur.selEnd().pit())
626 cur.message(_("Cannot index more than one paragraph!"));
628 idxstring = tmpcur.selectionAsString(false);
635 void Text::setParagraph(Cursor & cur,
636 Spacing const & spacing, LyXAlignment align,
637 docstring const & labelwidthstring, bool noindent)
639 BOOST_ASSERT(cur.text());
640 // make sure that the depth behind the selection are restored, too
641 pit_type undopit = undoSpan(cur.selEnd().pit());
642 recUndo(cur, cur.selBegin().pit(), undopit - 1);
644 for (pit_type pit = cur.selBegin().pit(), end = cur.selEnd().pit();
646 Paragraph & par = pars_[pit];
647 ParagraphParameters & params = par.params();
648 params.spacing(spacing);
650 // does the layout allow the new alignment?
651 Layout_ptr const & layout = par.layout();
653 if (align == LYX_ALIGN_LAYOUT)
654 align = layout->align;
655 if (align & layout->alignpossible) {
656 if (align == layout->align)
657 params.align(LYX_ALIGN_LAYOUT);
661 par.setLabelWidthString(labelwidthstring);
662 params.noindent(noindent);
667 // this really should just insert the inset and not move the cursor.
668 void Text::insertInset(Cursor & cur, Inset * inset)
670 BOOST_ASSERT(this == cur.text());
672 cur.paragraph().insertInset(cur.pos(), inset,
673 Change(cur.buffer().params().trackChanges ?
674 Change::INSERTED : Change::UNCHANGED));
678 // needed to insert the selection
679 void Text::insertStringAsLines(Cursor & cur, docstring const & str)
681 cur.buffer().insertStringAsLines(pars_, cur.pit(), cur.pos(),
682 current_font, str, autoBreakRows_);
686 // turn double CR to single CR, others are converted into one
687 // blank. Then insertStringAsLines is called
688 void Text::insertStringAsParagraphs(Cursor & cur, docstring const & str)
690 docstring linestr = str;
691 bool newline_inserted = false;
693 for (string::size_type i = 0, siz = linestr.size(); i < siz; ++i) {
694 if (linestr[i] == '\n') {
695 if (newline_inserted) {
696 // we know that \r will be ignored by
697 // insertStringAsLines. Of course, it is a dirty
698 // trick, but it works...
699 linestr[i - 1] = '\r';
703 newline_inserted = true;
705 } else if (isPrintable(linestr[i])) {
706 newline_inserted = false;
709 insertStringAsLines(cur, linestr);
713 bool Text::setCursor(Cursor & cur, pit_type par, pos_type pos,
714 bool setfont, bool boundary)
717 setCursorIntern(cur, par, pos, setfont, boundary);
718 return cur.bv().checkDepm(cur, old);
722 void Text::setCursor(CursorSlice & cur, pit_type par, pos_type pos)
724 BOOST_ASSERT(par != int(paragraphs().size()));
728 // now some strict checking
729 Paragraph & para = getPar(par);
731 // None of these should happen, but we're scaredy-cats
733 lyxerr << "dont like -1" << endl;
737 if (pos > para.size()) {
738 lyxerr << "dont like 1, pos: " << pos
739 << " size: " << para.size()
740 << " par: " << par << endl;
746 void Text::setCursorIntern(Cursor & cur,
747 pit_type par, pos_type pos, bool setfont, bool boundary)
749 BOOST_ASSERT(this == cur.text());
750 cur.boundary(boundary);
751 setCursor(cur.top(), par, pos);
757 void Text::setCurrentFont(Cursor & cur)
759 BOOST_ASSERT(this == cur.text());
760 pos_type pos = cur.pos();
761 Paragraph & par = cur.paragraph();
763 // ignore empty paragraph
767 // if on boundary or at paragraph end, set font of previous char
768 if ((pos > 0 && cur.boundary()) || pos == cur.lastpos())
771 // we changed the line and the bidi tables are outdated?
772 if (!bidi.inRange(pos))
773 bidi.computeTables(par, cur.buffer(), cur.textRow());
776 if (!bidi.inRange(pos))
780 if (pos == cur.lastpos())
782 else // potentional bug... BUG (Lgb)
783 if (par.isSeparator(pos)) {
784 if (pos > cur.textRow().pos() &&
785 bidi.level(pos) % 2 ==
786 bidi.level(pos - 1) % 2)
788 else if (pos + 1 < cur.lastpos())
793 BufferParams const & bufparams = cur.buffer().params();
794 current_font = par.getFontSettings(bufparams, pos);
795 real_current_font = getFont(cur.buffer(), par, pos);
797 if (cur.pos() == cur.lastpos()
798 && bidi.isBoundary(cur.buffer(), par, cur.pos())
799 && !cur.boundary()) {
800 Language const * lang = par.getParLanguage(bufparams);
801 current_font.setLanguage(lang);
802 current_font.setNumber(Font::OFF);
803 real_current_font.setLanguage(lang);
804 real_current_font.setNumber(Font::OFF);
808 // y is screen coordinate
809 pit_type Text::getPitNearY(BufferView & bv, int y) const
811 BOOST_ASSERT(!paragraphs().empty());
812 BOOST_ASSERT(bv.coordCache().getParPos().find(this) != bv.coordCache().getParPos().end());
813 CoordCache::InnerParPosCache const & cc = bv.coordCache().getParPos().find(this)->second;
815 << BOOST_CURRENT_FUNCTION
816 << ": y: " << y << " cache size: " << cc.size()
819 // look for highest numbered paragraph with y coordinate less than given y
822 CoordCache::InnerParPosCache::const_iterator it = cc.begin();
823 CoordCache::InnerParPosCache::const_iterator et = cc.end();
824 CoordCache::InnerParPosCache::const_iterator last = et; last--;
826 TextMetrics & tm = bv.textMetrics(this);
827 ParagraphMetrics const & pm = tm.parMetrics(it->first);
829 // If we are off-screen (before the visible part)
831 // and even before the first paragraph in the cache.
832 && y < it->second.y_ - int(pm.ascent())) {
833 // and we are not at the first paragraph in the inset.
836 // then this is the paragraph we are looking for.
838 // rebreak it and update the CoordCache.
839 tm.redoParagraph(pit);
840 bv.coordCache().parPos()[this][pit] =
841 Point(0, it->second.y_ - pm.descent());
845 ParagraphMetrics const & pm_last = bv.parMetrics(this, last->first);
847 // If we are off-screen (after the visible part)
848 if (y > bv.workHeight()
849 // and even after the first paragraph in the cache.
850 && y >= last->second.y_ + int(pm_last.descent())) {
851 pit = last->first + 1;
852 // and we are not at the last paragraph in the inset.
853 if (pit == int(pars_.size()))
855 // then this is the paragraph we are looking for.
856 // rebreak it and update the CoordCache.
857 tm.redoParagraph(pit);
858 bv.coordCache().parPos()[this][pit] =
859 Point(0, last->second.y_ + pm_last.ascent());
863 for (; it != et; ++it) {
865 << BOOST_CURRENT_FUNCTION
866 << " examining: pit: " << it->first
867 << " y: " << it->second.y_
870 ParagraphMetrics const & pm = bv.parMetrics(this, it->first);
872 if (it->first >= pit && int(it->second.y_) - int(pm.ascent()) <= y) {
879 << BOOST_CURRENT_FUNCTION
880 << ": found best y: " << yy << " for pit: " << pit
887 Row const & Text::getRowNearY(BufferView const & bv, int y, pit_type pit) const
889 ParagraphMetrics const & pm = bv.parMetrics(this, pit);
891 int yy = bv.coordCache().get(this, pit).y_ - pm.ascent();
892 BOOST_ASSERT(!pm.rows().empty());
893 RowList::const_iterator rit = pm.rows().begin();
894 RowList::const_iterator const rlast = boost::prior(pm.rows().end());
895 for (; rit != rlast; yy += rit->height(), ++rit)
896 if (yy + rit->height() > y)
902 // x,y are absolute screen coordinates
903 // sets cursor recursively descending into nested editable insets
904 Inset * Text::editXY(Cursor & cur, int x, int y)
906 if (lyxerr.debugging(Debug::WORKAREA)) {
907 lyxerr << "Text::editXY(cur, " << x << ", " << y << ")" << std::endl;
908 cur.bv().coordCache().dump();
910 pit_type pit = getPitNearY(cur.bv(), y);
911 BOOST_ASSERT(pit != -1);
913 Row const & row = getRowNearY(cur.bv(), y, pit);
916 TextMetrics const & tm = cur.bv().textMetrics(this);
917 int xx = x; // is modified by getColumnNearX
918 pos_type const pos = row.pos()
919 + tm.getColumnNearX(pit, row, xx, bound);
925 // try to descend into nested insets
926 Inset * inset = checkInsetHit(cur.bv(), x, y);
927 //lyxerr << "inset " << inset << " hit at x: " << x << " y: " << y << endl;
929 // Either we deconst editXY or better we move current_font
930 // and real_current_font to Cursor
935 Inset * insetBefore = pos? pars_[pit].getInset(pos - 1): 0;
936 //Inset * insetBehind = pars_[pit].getInset(pos);
938 // This should be just before or just behind the
939 // cursor position set above.
940 BOOST_ASSERT((pos != 0 && inset == insetBefore)
941 || inset == pars_[pit].getInset(pos));
943 // Make sure the cursor points to the position before
945 if (inset == insetBefore) {
950 // Try to descend recursively inside the inset.
951 inset = inset->editXY(cur, x, y);
953 if (cur.top().text() == this)
959 bool Text::checkAndActivateInset(Cursor & cur, bool front)
963 if (front && cur.pos() == cur.lastpos())
965 if (!front && cur.pos() == 0)
967 Inset * inset = front ? cur.nextInset() : cur.prevInset();
968 if (!isHighlyEditableInset(inset))
971 * Apparently, when entering an inset we are expected to be positioned
972 * *before* it in the containing paragraph, regardless of the direction
973 * from which we are entering. Otherwise, cursor placement goes awry,
974 * and when we exit from the beginning, we'll be placed *after* the
979 inset->edit(cur, front);
984 bool Text::cursorLeft(Cursor & cur)
986 // Tell BufferView to test for FitCursor in any case!
987 cur.updateFlags(Update::FitCursor);
989 // not at paragraph start?
991 // if on right side of boundary (i.e. not at paragraph end, but line end)
992 // -> skip it, i.e. set boundary to true, i.e. go only logically left
993 // there are some exceptions to ignore this: lineseps, newlines, spaces
995 // some effectless debug code to see the values in the debugger
996 bool bound = cur.boundary();
997 int rowpos = cur.textRow().pos();
999 bool sep = cur.paragraph().isSeparator(cur.pos() - 1);
1000 bool newline = cur.paragraph().isNewline(cur.pos() - 1);
1001 bool linesep = cur.paragraph().isLineSeparator(cur.pos() - 1);
1003 if (!cur.boundary() &&
1004 cur.textRow().pos() == cur.pos() &&
1005 !cur.paragraph().isLineSeparator(cur.pos() - 1) &&
1006 !cur.paragraph().isNewline(cur.pos() - 1) &&
1007 !cur.paragraph().isSeparator(cur.pos() - 1)) {
1008 return setCursor(cur, cur.pit(), cur.pos(), true, true);
1011 // go left and try to enter inset
1012 if (checkAndActivateInset(cur, false))
1015 // normal character left
1016 return setCursor(cur, cur.pit(), cur.pos() - 1, true, false);
1019 // move to the previous paragraph or do nothing
1021 return setCursor(cur, cur.pit() - 1, getPar(cur.pit() - 1).size());
1026 bool Text::cursorRight(Cursor & cur)
1028 // Tell BufferView to test for FitCursor in any case!
1029 cur.updateFlags(Update::FitCursor);
1031 // not at paragraph end?
1032 if (cur.pos() != cur.lastpos()) {
1033 // if left of boundary -> just jump to right side
1035 return setCursor(cur, cur.pit(), cur.pos(), true, false);
1037 // in front of editable inset, i.e. jump into it?
1038 if (checkAndActivateInset(cur, true))
1041 // next position is left of boundary,
1042 // but go to next line for special cases like space, newline, linesep
1044 // some effectless debug code to see the values in the debugger
1045 int endpos = cur.textRow().endpos();
1046 int lastpos = cur.lastpos();
1047 int pos = cur.pos();
1048 bool linesep = cur.paragraph().isLineSeparator(cur.pos());
1049 bool newline = cur.paragraph().isNewline(cur.pos());
1050 bool sep = cur.paragraph().isSeparator(cur.pos());
1051 if (cur.pos() != cur.lastpos()) {
1052 bool linesep2 = cur.paragraph().isLineSeparator(cur.pos()+1);
1053 bool newline2 = cur.paragraph().isNewline(cur.pos()+1);
1054 bool sep2 = cur.paragraph().isSeparator(cur.pos()+1);
1057 if (cur.textRow().endpos() == cur.pos() + 1 &&
1058 cur.textRow().endpos() != cur.lastpos() &&
1059 !cur.paragraph().isNewline(cur.pos()) &&
1060 !cur.paragraph().isLineSeparator(cur.pos()) &&
1061 !cur.paragraph().isSeparator(cur.pos())) {
1062 return setCursor(cur, cur.pit(), cur.pos() + 1, true, true);
1066 return setCursor(cur, cur.pit(), cur.pos() + 1, true, false);
1069 // move to next paragraph
1070 if (cur.pit() != cur.lastpit())
1071 return setCursor(cur, cur.pit() + 1, 0);
1076 bool Text::cursorUpParagraph(Cursor & cur)
1078 bool updated = false;
1080 updated = setCursor(cur, cur.pit(), 0);
1081 else if (cur.pit() != 0)
1082 updated = setCursor(cur, cur.pit() - 1, 0);
1087 bool Text::cursorDownParagraph(Cursor & cur)
1089 bool updated = false;
1090 if (cur.pit() != cur.lastpit())
1091 updated = setCursor(cur, cur.pit() + 1, 0);
1093 updated = setCursor(cur, cur.pit(), cur.lastpos());
1098 // fix the cursor `cur' after a characters has been deleted at `where'
1099 // position. Called by deleteEmptyParagraphMechanism
1100 void Text::fixCursorAfterDelete(CursorSlice & cur, CursorSlice const & where)
1102 // Do nothing if cursor is not in the paragraph where the
1103 // deletion occured,
1104 if (cur.pit() != where.pit())
1107 // If cursor position is after the deletion place update it
1108 if (cur.pos() > where.pos())
1111 // Check also if we don't want to set the cursor on a spot behind the
1112 // pagragraph because we erased the last character.
1113 if (cur.pos() > cur.lastpos())
1114 cur.pos() = cur.lastpos();
1118 bool Text::deleteEmptyParagraphMechanism(Cursor & cur,
1119 Cursor & old, bool & need_anchor_change)
1121 //LYXERR(Debug::DEBUG) << "DEPM: cur:\n" << cur << "old:\n" << old << endl;
1123 Paragraph & oldpar = old.paragraph();
1125 // We allow all kinds of "mumbo-jumbo" when freespacing.
1126 if (oldpar.isFreeSpacing())
1129 /* Ok I'll put some comments here about what is missing.
1130 There are still some small problems that can lead to
1131 double spaces stored in the document file or space at
1132 the beginning of paragraphs(). This happens if you have
1133 the cursor between to spaces and then save. Or if you
1134 cut and paste and the selection have a space at the
1135 beginning and then save right after the paste. (Lgb)
1138 // If old.pos() == 0 and old.pos()(1) == LineSeparator
1139 // delete the LineSeparator.
1142 // If old.pos() == 1 and old.pos()(0) == LineSeparator
1143 // delete the LineSeparator.
1146 bool const same_inset = &old.inset() == &cur.inset();
1147 bool const same_par = same_inset && old.pit() == cur.pit();
1148 bool const same_par_pos = same_par && old.pos() == cur.pos();
1150 // If the chars around the old cursor were spaces, delete one of them.
1151 if (!same_par_pos) {
1152 // Only if the cursor has really moved.
1154 && old.pos() < oldpar.size()
1155 && oldpar.isLineSeparator(old.pos())
1156 && oldpar.isLineSeparator(old.pos() - 1)
1157 && !oldpar.isDeleted(old.pos() - 1)) {
1158 oldpar.eraseChar(old.pos() - 1, cur.buffer().params().trackChanges);
1159 #ifdef WITH_WARNINGS
1160 #warning This will not work anymore when we have multiple views of the same buffer
1161 // In this case, we will have to correct also the cursors held by
1162 // other bufferviews. It will probably be easier to do that in a more
1163 // automated way in CursorSlice code. (JMarc 26/09/2001)
1165 // correct all cursor parts
1167 fixCursorAfterDelete(cur.top(), old.top());
1168 need_anchor_change = true;
1174 // only do our magic if we changed paragraph
1178 // don't delete anything if this is the ONLY paragraph!
1179 if (old.lastpit() == 0)
1182 // Do not delete empty paragraphs with keepempty set.
1183 if (oldpar.allowEmpty())
1186 if (oldpar.empty() || (oldpar.size() == 1 && oldpar.isLineSeparator(0))) {
1188 recordUndo(old, Undo::ATOMIC,
1189 max(old.pit() - 1, pit_type(0)),
1190 min(old.pit() + 1, old.lastpit()));
1191 ParagraphList & plist = old.text()->paragraphs();
1192 plist.erase(boost::next(plist.begin(), old.pit()));
1194 // see #warning above
1195 if (cur.depth() >= old.depth()) {
1196 CursorSlice & curslice = cur[old.depth() - 1];
1197 if (&curslice.inset() == &old.inset()
1198 && curslice.pit() > old.pit()) {
1200 // since a paragraph has been deleted, all the
1201 // insets after `old' have been copied and
1202 // their address has changed. Therefore we
1203 // need to `regenerate' cur. (JMarc)
1204 cur.updateInsets(&(cur.bottom().inset()));
1205 need_anchor_change = true;
1211 if (oldpar.stripLeadingSpaces(cur.buffer().params().trackChanges)) {
1212 need_anchor_change = true;
1213 // We return true here because the Paragraph contents changed and
1214 // we need a redraw before further action is processed.
1222 void Text::deleteEmptyParagraphMechanism(pit_type first, pit_type last, bool trackChanges)
1224 BOOST_ASSERT(first >= 0 && first <= last && last < (int) pars_.size());
1226 for (pit_type pit = first; pit <= last; ++pit) {
1227 Paragraph & par = pars_[pit];
1229 // We allow all kinds of "mumbo-jumbo" when freespacing.
1230 if (par.isFreeSpacing())
1233 for (pos_type pos = 1; pos < par.size(); ++pos) {
1234 if (par.isLineSeparator(pos) && par.isLineSeparator(pos - 1)
1235 && !par.isDeleted(pos - 1)) {
1236 if (par.eraseChar(pos - 1, trackChanges)) {
1242 // don't delete anything if this is the only remaining paragraph within the given range
1243 // note: Text::acceptOrRejectChanges() sets the cursor to 'first' after calling DEPM
1247 // don't delete empty paragraphs with keepempty set
1248 if (par.allowEmpty())
1251 if (par.empty() || (par.size() == 1 && par.isLineSeparator(0))) {
1252 pars_.erase(boost::next(pars_.begin(), pit));
1258 par.stripLeadingSpaces(trackChanges);
1263 void Text::recUndo(Cursor & cur, pit_type first, pit_type last) const
1265 recordUndo(cur, Undo::ATOMIC, first, last);
1269 void Text::recUndo(Cursor & cur, pit_type par) const
1271 recordUndo(cur, Undo::ATOMIC, par, par);