diff --git a/src/view/kateviewinternal.cpp b/src/view/kateviewinternal.cpp index 2c6d529f..7a263159 100644 --- a/src/view/kateviewinternal.cpp +++ b/src/view/kateviewinternal.cpp @@ -1,3873 +1,3856 @@ /* This file is part of the KDE libraries Copyright (C) 2002 John Firebaugh Copyright (C) 2002 Joseph Wenninger Copyright (C) 2002,2003 Christoph Cullmann Copyright (C) 2002-2007 Hamish Rodda Copyright (C) 2003 Anakim Border Copyright (C) 2007 Mirko Stocker Copyright (C) 2007 Matthew Woehlke Copyright (C) 2008 Erlend Hamberg Based on: KWriteView : Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateviewinternal.h" #include "kateview.h" #include "kateviewhelpers.h" #include "katehighlight.h" #include "katebuffer.h" #include "katerenderer.h" #include "kateconfig.h" #include "katelayoutcache.h" #include "katecompletionwidget.h" #include "spellcheck/spellingmenu.h" #include "kateviewaccessible.h" #include "katetextanimation.h" #include "katemessagewidget.h" #include "kateglobal.h" #include "kateabstractinputmodefactory.h" #include "kateabstractinputmode.h" #include "katepartdebug.h" #include "inlinenotedata.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const bool debugPainting = false; class ZoomEventFilter { public: ZoomEventFilter() = default; bool detectZoomingEvent(QWheelEvent *e, Qt::KeyboardModifiers modifier = Qt::ControlModifier) { Qt::KeyboardModifiers modState = e->modifiers(); if (modState == modifier) { if (m_lastWheelEvent.isValid()) { const qint64 deltaT = m_lastWheelEvent.elapsed(); // Pressing the specified modifier key within 200ms of the previous "unmodified" // wheelevent is not allowed to toggle on text zooming if (m_lastWheelEventUnmodified && deltaT < 200) { m_ignoreZoom = true; } else if (deltaT > 1000) { // the protection is kept active for 1s after the last wheel event // TODO: this value should be tuned, preferably by someone using // Ctrl+Wheel zooming frequently. m_ignoreZoom = false; } } else { // we can't say anything and have to assume there's nothing // accidental to the modifier being pressed. m_ignoreZoom = false; } m_lastWheelEventUnmodified = false; if (m_ignoreZoom) { // unset the modifier so the view scrollbars can handle the scroll // event and produce normal, not accelerated scrolling modState &= ~modifier; e->setModifiers(modState); } } else { // state is reset after any wheel event without the zoom modifier m_lastWheelEventUnmodified = true; m_ignoreZoom = false; } m_lastWheelEvent.start(); // inform the caller whether this event is allowed to trigger text zooming. return !m_ignoreZoom && modState == modifier; } protected: QElapsedTimer m_lastWheelEvent; bool m_ignoreZoom = false; bool m_lastWheelEventUnmodified = false; }; KateViewInternal::KateViewInternal(KTextEditor::ViewPrivate *view) : QWidget(view) , editSessionNumber(0) , editIsRunning(false) , m_view(view) , m_cursor(doc()->buffer(), KTextEditor::Cursor(0, 0), Kate::TextCursor::MoveOnInsert) , m_mouse() , m_possibleTripleClick(false) , m_completionItemExpanded(false) , m_bm(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand)) , m_bmStart(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand)) , m_bmEnd(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand)) , m_bmLastFlashPos(doc()->newMovingCursor(KTextEditor::Cursor::invalid())) , m_dummy(nullptr) // stay on cursor will avoid that the view scroll around on press return at beginning , m_startPos(doc()->buffer(), KTextEditor::Cursor(0, 0), Kate::TextCursor::StayOnInsert) , m_visibleLineCount(0) , m_madeVisible(false) , m_shiftKeyPressed(false) , m_autoCenterLines(0) , m_minLinesVisible(0) , m_selChangedByUser(false) , m_selectAnchor(-1, -1) , m_selectionMode(Default) , m_layoutCache(new KateLayoutCache(renderer(), this)) , m_preserveX(false) , m_preservedX(0) , m_cachedMaxStartPos(-1, -1) , m_dragScrollTimer(this) , m_scrollTimer(this) , m_cursorTimer(this) , m_textHintTimer(this) , m_textHintDelay(500) , m_textHintPos(-1, -1) , m_imPreeditRange(nullptr) { QList factories = KTextEditor::EditorPrivate::self()->inputModeFactories(); Q_FOREACH(KateAbstractInputModeFactory *factory, factories) { KateAbstractInputMode *m = factory->createInputMode(this); m_inputModes.insert(m->viewInputMode(), m); } m_currentInputMode = m_inputModes[KTextEditor::View::NormalInputMode]; // TODO: twisted, but needed setMinimumSize(0, 0); setAttribute(Qt::WA_OpaquePaintEvent); setAttribute(Qt::WA_InputMethodEnabled); // invalidate m_selectionCached.start(), or keyb selection is screwed initially m_selectionCached = KTextEditor::Range::invalid(); // bracket markers are only for this view and should not be printed m_bm->setView(m_view); m_bmStart->setView(m_view); m_bmEnd->setView(m_view); m_bm->setAttributeOnlyForViews(true); m_bmStart->setAttributeOnlyForViews(true); m_bmEnd->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface m_bm->setZDepth(-1000.0); m_bmStart->setZDepth(-1000.0); m_bmEnd->setZDepth(-1000.0); // update mark attributes updateBracketMarkAttributes(); // // scrollbar for lines // m_lineScroll = new KateScrollBar(Qt::Vertical, this); m_lineScroll->show(); m_lineScroll->setTracking(true); m_lineScroll->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); // Hijack the line scroller's controls, so we can scroll nicely for word-wrap connect(m_lineScroll, SIGNAL(actionTriggered(int)), SLOT(scrollAction(int))); connect(m_lineScroll, SIGNAL(sliderMoved(int)), SLOT(scrollLines(int))); connect(m_lineScroll, SIGNAL(sliderMMBMoved(int)), SLOT(scrollLines(int))); connect(m_lineScroll, SIGNAL(valueChanged(int)), SLOT(scrollLines(int))); // // scrollbar for columns // m_columnScroll = new QScrollBar(Qt::Horizontal, m_view); if (m_view->dynWordWrap()) { m_columnScroll->hide(); } else { m_columnScroll->show(); } m_columnScroll->setTracking(true); m_startX = 0; connect(m_columnScroll, SIGNAL(valueChanged(int)), SLOT(scrollColumns(int))); // bottom corner box m_dummy = new QWidget(m_view); m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height()); m_dummy->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); if (m_view->dynWordWrap()) { m_dummy->hide(); } else { m_dummy->show(); } cache()->setWrap(m_view->dynWordWrap()); // // iconborder ;) // m_leftBorder = new KateIconBorder(this, m_view); m_leftBorder->show(); // update view if folding ranges change connect(&m_view->textFolding(), SIGNAL(foldingRangesChanged()), SLOT(slotRegionVisibilityChanged())); m_displayCursor.setPosition(0, 0); setAcceptDrops(true); m_zoomEventFilter = new ZoomEventFilter(); // event filter installEventFilter(this); // set initial cursor m_mouseCursor = Qt::IBeamCursor; setCursor(m_mouseCursor); // call mouseMoveEvent also if no mouse button is pressed setMouseTracking(true); m_dragInfo.state = diNone; // timers connect(&m_dragScrollTimer, SIGNAL(timeout()), this, SLOT(doDragScroll())); connect(&m_scrollTimer, SIGNAL(timeout()), this, SLOT(scrollTimeout())); connect(&m_cursorTimer, SIGNAL(timeout()), this, SLOT(cursorTimeout())); connect(&m_textHintTimer, SIGNAL(timeout()), this, SLOT(textHintTimeout())); // selection changed to set anchor connect(m_view, SIGNAL(selectionChanged(KTextEditor::View*)), this, SLOT(viewSelectionChanged())); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(accessibleInterfaceFactory); #endif connect(doc(), &KTextEditor::DocumentPrivate::textInserted, this, &KateViewInternal::documentTextInserted); connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &KateViewInternal::documentTextRemoved); // update is called in KTextEditor::ViewPrivate, after construction and layout is over // but before any other kateviewinternal call } KateViewInternal::~KateViewInternal() { // delete text animation object here, otherwise it updates the view in its destructor delete m_textAnimation; #ifndef QT_NO_ACCESSIBILITY QAccessible::removeFactory(accessibleInterfaceFactory); #endif // kill preedit ranges delete m_imPreeditRange; qDeleteAll(m_imPreeditRangeChildren); qDeleteAll(m_inputModes); // delete bracket markers delete m_bm; delete m_bmStart; delete m_bmEnd; delete m_zoomEventFilter; } void KateViewInternal::prepareForDynWrapChange() { // Which is the current view line? m_wrapChangeViewLine = cache()->displayViewLine(m_displayCursor, true); } void KateViewInternal::dynWrapChanged() { m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height()); if (view()->dynWordWrap()) { m_columnScroll->hide(); m_dummy->hide(); } else { // column scrollbar + bottom corner box m_columnScroll->show(); m_dummy->show(); } cache()->setWrap(view()->dynWordWrap()); updateView(); if (view()->dynWordWrap()) { scrollColumns(0); } // Determine where the cursor should be to get the cursor on the same view line if (m_wrapChangeViewLine != -1) { KTextEditor::Cursor newStart = viewLineOffset(m_displayCursor, -m_wrapChangeViewLine); makeVisible(newStart, newStart.column(), true); } else { update(); } } KTextEditor::Cursor KateViewInternal::endPos() const { // Hrm, no lines laid out at all?? if (!cache()->viewCacheLineCount()) { return KTextEditor::Cursor(); } for (int i = qMin(linesDisplayed() - 1, cache()->viewCacheLineCount() - 1); i >= 0; i--) { const KateTextLayout &thisLine = cache()->viewLine(i); if (thisLine.line() == -1) { continue; } if (thisLine.virtualLine() >= view()->textFolding().visibleLines()) { // Cache is too out of date return KTextEditor::Cursor(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); } return KTextEditor::Cursor(thisLine.virtualLine(), thisLine.wrap() ? thisLine.endCol() - 1 : thisLine.endCol()); } // can happen, if view is still invisible return KTextEditor::Cursor(); } int KateViewInternal::endLine() const { return endPos().line(); } KateTextLayout KateViewInternal::yToKateTextLayout(int y) const { if (y < 0 || y > size().height()) { return KateTextLayout::invalid(); } int range = y / renderer()->lineHeight(); // lineRanges is always bigger than 0, after the initial updateView call if (range >= 0 && range < cache()->viewCacheLineCount()) { return cache()->viewLine(range); } return KateTextLayout::invalid(); } int KateViewInternal::lineToY(int viewLine) const { return (viewLine - startLine()) * renderer()->lineHeight(); } void KateViewInternal::slotIncFontSizes(qreal step) { renderer()->increaseFontSizes(step); } void KateViewInternal::slotDecFontSizes(qreal step) { renderer()->decreaseFontSizes(step); } /** * Line is the real line number to scroll to. */ void KateViewInternal::scrollLines(int line) { KTextEditor::Cursor newPos(line, 0); scrollPos(newPos); } // This can scroll less than one true line void KateViewInternal::scrollViewLines(int offset) { KTextEditor::Cursor c = viewLineOffset(startPos(), offset); scrollPos(c); bool blocked = m_lineScroll->blockSignals(true); m_lineScroll->setValue(startLine()); m_lineScroll->blockSignals(blocked); } void KateViewInternal::scrollAction(int action) { switch (action) { case QAbstractSlider::SliderSingleStepAdd: scrollNextLine(); break; case QAbstractSlider::SliderSingleStepSub: scrollPrevLine(); break; case QAbstractSlider::SliderPageStepAdd: scrollNextPage(); break; case QAbstractSlider::SliderPageStepSub: scrollPrevPage(); break; case QAbstractSlider::SliderToMinimum: top_home(); break; case QAbstractSlider::SliderToMaximum: bottom_end(); break; } } void KateViewInternal::scrollNextPage() { scrollViewLines(qMax(linesDisplayed() - 1, 0)); } void KateViewInternal::scrollPrevPage() { scrollViewLines(-qMax(linesDisplayed() - 1, 0)); } void KateViewInternal::scrollPrevLine() { scrollViewLines(-1); } void KateViewInternal::scrollNextLine() { scrollViewLines(1); } KTextEditor::Cursor KateViewInternal::maxStartPos(bool changed) { cache()->setAcceptDirtyLayouts(true); if (m_cachedMaxStartPos.line() == -1 || changed) { KTextEditor::Cursor end(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); if (view()->config()->scrollPastEnd()) { m_cachedMaxStartPos = viewLineOffset(end, -m_minLinesVisible); } else { m_cachedMaxStartPos = viewLineOffset(end, -(linesDisplayed() - 1)); } } cache()->setAcceptDirtyLayouts(false); return m_cachedMaxStartPos; } // c is a virtual cursor void KateViewInternal::scrollPos(KTextEditor::Cursor &c, bool force, bool calledExternally, bool emitSignals) { if (!force && ((!view()->dynWordWrap() && c.line() == startLine()) || c == startPos())) { return; } if (c.line() < 0) { c.setLine(0); } KTextEditor::Cursor limit = maxStartPos(); if (c > limit) { c = limit; // Re-check we're not just scrolling to the same place if (!force && ((!view()->dynWordWrap() && c.line() == startLine()) || c == startPos())) { return; } } int viewLinesScrolled = 0; // only calculate if this is really used and useful, could be wrong here, please recheck // for larger scrolls this makes 2-4 seconds difference on my xeon with dyn. word wrap on // try to get it really working ;) bool viewLinesScrolledUsable = !force && (c.line() >= startLine() - linesDisplayed() - 1) && (c.line() <= endLine() + linesDisplayed() + 1); if (viewLinesScrolledUsable) { viewLinesScrolled = cache()->displayViewLine(c); } m_startPos.setPosition(c); // set false here but reversed if we return to makeVisible m_madeVisible = false; if (viewLinesScrolledUsable) { int lines = linesDisplayed(); if (view()->textFolding().visibleLines() < lines) { KTextEditor::Cursor end(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); lines = qMin(linesDisplayed(), cache()->displayViewLine(end) + 1); } Q_ASSERT(lines >= 0); if (!calledExternally && qAbs(viewLinesScrolled) < lines && // NOTE: on some machines we must update if the floating widget is visible // otherwise strange painting bugs may occur during scrolling... !((view()->m_messageWidgets[KTextEditor::Message::TopInView] && view()->m_messageWidgets[KTextEditor::Message::TopInView]->isVisible()) ||(view()->m_messageWidgets[KTextEditor::Message::CenterInView] && view()->m_messageWidgets[KTextEditor::Message::CenterInView]->isVisible()) ||(view()->m_messageWidgets[KTextEditor::Message::BottomInView] && view()->m_messageWidgets[KTextEditor::Message::BottomInView]->isVisible()) ) ) { updateView(false, viewLinesScrolled); int scrollHeight = -(viewLinesScrolled * (int)renderer()->lineHeight()); // scroll excluding child widgets (floating notifications) scroll(0, scrollHeight, rect()); m_leftBorder->scroll(0, scrollHeight); if (emitSignals) { emit view()->verticalScrollPositionChanged(m_view, c); emit view()->displayRangeChanged(m_view); } return; } } updateView(); update(); m_leftBorder->update(); if (emitSignals) { emit view()->verticalScrollPositionChanged(m_view, c); emit view()->displayRangeChanged(m_view); } } void KateViewInternal::scrollColumns(int x) { if (x < 0) { x = 0; } if (x > m_columnScroll->maximum()) { x = m_columnScroll->maximum(); } if (x == startX()) { return; } int dx = startX() - x; m_startX = x; if (qAbs(dx) < width()) { // scroll excluding child widgets (floating notifications) scroll(dx, 0, rect()); } else { update(); } emit view()->horizontalScrollPositionChanged(m_view); emit view()->displayRangeChanged(m_view); bool blocked = m_columnScroll->blockSignals(true); m_columnScroll->setValue(startX()); m_columnScroll->blockSignals(blocked); } // If changed is true, the lines that have been set dirty have been updated. void KateViewInternal::updateView(bool changed, int viewLinesScrolled) { if (!isVisible() && !viewLinesScrolled && !changed) { return; //When this view is not visible, don't do anything } view()->doc()->delayAutoReload(); // Don't reload while user scrolls around bool blocked = m_lineScroll->blockSignals(true); int wrapWidth = width(); if (view()->config()->dynWrapAtStaticMarker() && view()->config()->dynWordWrap()) { // We need to transform char count to a pixel width, stolen from PrintPainter::updateCache() QString s; s.fill(QLatin1Char('5'), view()->doc()->config()->wordWrapAt()); wrapWidth = qMin(width(), static_cast(renderer()->currentFontMetrics().width(s))); } if (wrapWidth != cache()->viewWidth()) { cache()->setViewWidth(wrapWidth); changed = true; } /* It was observed that height() could be negative here -- when the main Kate view has 0 as size (during creation), and there frame around KateViewInternal. In which case we'd set the view cache to 0 (or less!) lines, and start allocating huge chunks of data, later. */ int newSize = (qMax(0, height()) / renderer()->lineHeight()) + 1; cache()->updateViewCache(startPos(), newSize, viewLinesScrolled); m_visibleLineCount = newSize; KTextEditor::Cursor maxStart = maxStartPos(changed); int maxLineScrollRange = maxStart.line(); if (view()->dynWordWrap() && maxStart.column() != 0) { maxLineScrollRange++; } m_lineScroll->setRange(0, maxLineScrollRange); m_lineScroll->setValue(startLine()); m_lineScroll->setSingleStep(1); m_lineScroll->setPageStep(qMax(0, height()) / renderer()->lineHeight()); m_lineScroll->blockSignals(blocked); KateViewConfig::ScrollbarMode show_scrollbars = static_cast(view()->config()->showScrollbars()); bool visible = ((show_scrollbars == KateViewConfig::AlwaysOn) || ((show_scrollbars == KateViewConfig::ShowWhenNeeded) && (maxLineScrollRange != 0))); bool visible_dummy = visible; m_lineScroll->setVisible(visible); if (!view()->dynWordWrap()) { int max = maxLen(startLine()) - width(); if (max < 0) { max = 0; } // if we lose the ability to scroll horizontally, move view to the far-left if (max == 0) { scrollColumns(0); } blocked = m_columnScroll->blockSignals(true); // disable scrollbar m_columnScroll->setDisabled(max == 0); visible = ((show_scrollbars == KateViewConfig::AlwaysOn) || ((show_scrollbars == KateViewConfig::ShowWhenNeeded) && (max != 0))); visible_dummy &= visible; m_columnScroll->setVisible(visible); m_columnScroll->setRange(0, max + (renderer()->spaceWidth() / 2)); // Add some space for the caret at EOL m_columnScroll->setValue(startX()); // Approximate linescroll m_columnScroll->setSingleStep(renderer()->config()->fontMetrics().width(QLatin1Char('a'))); m_columnScroll->setPageStep(width()); m_columnScroll->blockSignals(blocked); } else { visible_dummy = false; } m_dummy->setVisible(visible_dummy); if (changed) { updateDirty(); } } /** * this function ensures a certain location is visible on the screen. * if endCol is -1, ignore making the columns visible. */ void KateViewInternal::makeVisible(const KTextEditor::Cursor &c, int endCol, bool force, bool center, bool calledExternally) { //qCDebug(LOG_KTE) << "MakeVisible start " << startPos() << " end " << endPos() << " -> request: " << c;// , new start [" << scroll.line << "," << scroll.col << "] lines " << (linesDisplayed() - 1) << " height " << height(); // if the line is in a folded region, unfold all the way up //if ( doc()->foldingTree()->findNodeForLine( c.line )->visible ) // qCDebug(LOG_KTE)<<"line ("< endPos())) { KTextEditor::Cursor scroll = viewLineOffset(c, -int(linesDisplayed()) / 2); scrollPos(scroll, false, calledExternally); } else if (c > viewLineOffset(startPos(), linesDisplayed() - m_minLinesVisible - 1)) { KTextEditor::Cursor scroll = viewLineOffset(c, -(linesDisplayed() - m_minLinesVisible - 1)); scrollPos(scroll, false, calledExternally); } else if (c < viewLineOffset(startPos(), m_minLinesVisible)) { KTextEditor::Cursor scroll = viewLineOffset(c, -m_minLinesVisible); scrollPos(scroll, false, calledExternally); } else { // Check to see that we're not showing blank lines KTextEditor::Cursor max = maxStartPos(); if (startPos() > max) { scrollPos(max, max.column(), calledExternally); } } if (!view()->dynWordWrap() && (endCol != -1 || view()->wrapCursor())) { KTextEditor::Cursor rc = toRealCursor(c); int sX = renderer()->cursorToX(cache()->textLayout(rc), rc, !view()->wrapCursor()); int sXborder = sX - 8; if (sXborder < 0) { sXborder = 0; } if (sX < startX()) { scrollColumns(sXborder); } else if (sX > startX() + width()) { scrollColumns(sX - width() + 8); } } m_madeVisible = !force; #ifndef QT_NO_ACCESSIBILITY // FIXME -- is this needed? // QAccessible::updateAccessibility(this, KateCursorAccessible::ChildId, QAccessible::Focus); #endif } void KateViewInternal::slotRegionVisibilityChanged() { qCDebug(LOG_KTE); cache()->clear(); m_cachedMaxStartPos.setLine(-1); KTextEditor::Cursor max = maxStartPos(); if (startPos() > max) { scrollPos(max, false, false, false /* don't emit signals! */); } // if text was folded: make sure the cursor is on a visible line qint64 foldedRangeId = -1; if (!view()->textFolding().isLineVisible(m_cursor.line(), &foldedRangeId)) { KTextEditor::Range foldingRange = view()->textFolding().foldingRange(foldedRangeId); Q_ASSERT(foldingRange.start().isValid()); // set cursor to start of folding region updateCursor(foldingRange.start(), true); } else { // force an update of the cursor, since otherwise the m_displayCursor // line may be below the total amount of visible lines. updateCursor(m_cursor, true); } updateView(); update(); m_leftBorder->update(); // emit signals here, scrollPos has this disabled, to ensure we do this after all stuff is updated! emit view()->verticalScrollPositionChanged(m_view, max); emit view()->displayRangeChanged(m_view); } void KateViewInternal::slotRegionBeginEndAddedRemoved(unsigned int) { qCDebug(LOG_KTE); // FIXME: performance problem m_leftBorder->update(); } void KateViewInternal::showEvent(QShowEvent *e) { updateView(); QWidget::showEvent(e); } int KateViewInternal::linesDisplayed() const { int h = height(); // catch zero heights, even if should not happen int fh = qMax(1, renderer()->lineHeight()); // default to 1, there is always one line around.... // too many places calc with linesDisplayed() - 1 return qMax(1, (h - (h % fh)) / fh); } QPoint KateViewInternal::cursorToCoordinate(const KTextEditor::Cursor &cursor, bool realCursor, bool includeBorder) const { if (cursor.line() >= doc()->lines()) { return QPoint(-1, -1); } int viewLine = cache()->displayViewLine(realCursor ? toVirtualCursor(cursor) : cursor, true); if (viewLine < 0 || viewLine >= cache()->viewCacheLineCount()) { return QPoint(-1, -1); } const int y = (int)viewLine * renderer()->lineHeight(); KateTextLayout layout = cache()->viewLine(viewLine); if (cursor.column() > doc()->lineLength(cursor.line())) { return QPoint(-1, -1); } int x = 0; // only set x value if we have a valid layout (bug #171027) if (layout.isValid()) { x = (int)layout.lineLayout().cursorToX(cursor.column()); } // else // qCDebug(LOG_KTE) << "Invalid Layout"; if (includeBorder) { x += m_leftBorder->width(); } x -= startX(); return QPoint(x, y); } QPoint KateViewInternal::cursorCoordinates(bool includeBorder) const { return cursorToCoordinate(m_displayCursor, false, includeBorder); } KTextEditor::Cursor KateViewInternal::findMatchingBracket() { KTextEditor::Cursor c; if (!m_bm->toRange().isValid()) { return KTextEditor::Cursor::invalid(); } Q_ASSERT(m_bmEnd->toRange().isValid()); Q_ASSERT(m_bmStart->toRange().isValid()); if (m_bmStart->toRange().contains(m_cursor) || m_bmStart->end() == m_cursor.toCursor()) { c = m_bmEnd->end(); // We need to adjust the cursor positon in case of override mode, BUG-402594 if (doc()->config()->ovr()) { c.setColumn(c.column() - 1); } } else if (m_bmEnd->toRange().contains(m_cursor) || m_bmEnd->end() == m_cursor.toCursor()) { c = m_bmStart->start(); } else { // should never happen: a range exists, but the cursor position is // neither at the start nor at the end... return KTextEditor::Cursor::invalid(); } return c; } class CalculatingCursor { public: // These constructors constrain their arguments to valid positions // before only the third one did, but that leads to crashs // see bug 227449 CalculatingCursor(KateViewInternal *vi) : m_vi(vi) { makeValid(); } CalculatingCursor(KateViewInternal *vi, const KTextEditor::Cursor &c) : m_cursor(c) , m_vi(vi) { makeValid(); } CalculatingCursor(KateViewInternal *vi, int line, int col) : m_cursor(line, col) , m_vi(vi) { makeValid(); } virtual ~CalculatingCursor() { } int line() const { return m_cursor.line(); } int column() const { return m_cursor.column(); } operator KTextEditor::Cursor() const { return m_cursor; } virtual CalculatingCursor &operator+=(int n) = 0; virtual CalculatingCursor &operator-=(int n) = 0; CalculatingCursor &operator++() { return operator+=(1); } CalculatingCursor &operator--() { return operator-=(1); } void makeValid() { m_cursor.setLine(qBound(0, line(), int(doc()->lines() - 1))); if (view()->wrapCursor()) { m_cursor.setColumn(qBound(0, column(), doc()->lineLength(line()))); } else { m_cursor.setColumn(qMax(0, column())); } Q_ASSERT(valid()); } void toEdge(KateViewInternal::Bias bias) { if (bias == KateViewInternal::left) { m_cursor.setColumn(0); } else if (bias == KateViewInternal::right) { m_cursor.setColumn(doc()->lineLength(line())); } } bool atEdge() const { return atEdge(KateViewInternal::left) || atEdge(KateViewInternal::right); } bool atEdge(KateViewInternal::Bias bias) const { switch (bias) { case KateViewInternal::left: return column() == 0; case KateViewInternal::none: return atEdge(); case KateViewInternal::right: return column() >= doc()->lineLength(line()); default: Q_ASSERT(false); return false; } } protected: bool valid() const { return line() >= 0 && line() < doc()->lines() && column() >= 0 && (!view()->wrapCursor() || column() <= doc()->lineLength(line())); } KTextEditor::ViewPrivate *view() { return m_vi->m_view; } const KTextEditor::ViewPrivate *view() const { return m_vi->m_view; } KTextEditor::DocumentPrivate *doc() { return view()->doc(); } const KTextEditor::DocumentPrivate *doc() const { return view()->doc(); } KTextEditor::Cursor m_cursor; KateViewInternal *m_vi; }; class BoundedCursor : public CalculatingCursor { public: BoundedCursor(KateViewInternal *vi) : CalculatingCursor(vi) {} BoundedCursor(KateViewInternal *vi, const KTextEditor::Cursor &c) : CalculatingCursor(vi, c) {} BoundedCursor(KateViewInternal *vi, int line, int col) : CalculatingCursor(vi, line, col) {} CalculatingCursor &operator+=(int n) override { KateLineLayoutPtr thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve valid layout for line " << line(); return *this; } const bool wrapCursor = view()->wrapCursor(); int maxColumn = -1; if (n >= 0) { for (int i = 0; i < n; i++) { if (column() >= thisLine->length()) { if (wrapCursor) { break; } else if (view()->dynWordWrap()) { // Don't go past the edge of the screen in dynamic wrapping mode if (maxColumn == -1) { maxColumn = thisLine->length() + ((m_vi->width() - thisLine->widthOfLastLine()) / m_vi->renderer()->spaceWidth()) - 1; } if (column() >= maxColumn) { m_cursor.setColumn(maxColumn); break; } m_cursor.setColumn(column() + 1); } else { m_cursor.setColumn(column() + 1); } } else { m_cursor.setColumn(thisLine->layout()->nextCursorPosition(column())); } } } else { for (int i = 0; i > n; i--) { if (column() >= thisLine->length()) { m_cursor.setColumn(column() - 1); } else if (column() == 0) { break; } else { m_cursor.setColumn(thisLine->layout()->previousCursorPosition(column())); } } } Q_ASSERT(valid()); return *this; } CalculatingCursor &operator-=(int n) override { return operator+=(-n); } }; class WrappingCursor : public CalculatingCursor { public: WrappingCursor(KateViewInternal *vi) : CalculatingCursor(vi) {} WrappingCursor(KateViewInternal *vi, const KTextEditor::Cursor &c) : CalculatingCursor(vi, c) {} WrappingCursor(KateViewInternal *vi, int line, int col) : CalculatingCursor(vi, line, col) {} CalculatingCursor &operator+=(int n) override { KateLineLayoutPtr thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line(); return *this; } if (n >= 0) { for (int i = 0; i < n; i++) { if (column() >= thisLine->length()) { // Have come to the end of a line if (line() >= doc()->lines() - 1) // Have come to the end of the document { break; } // Advance to the beginning of the next line m_cursor.setColumn(0); m_cursor.setLine(line() + 1); // Retrieve the next text range thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line(); return *this; } continue; } m_cursor.setColumn(thisLine->layout()->nextCursorPosition(column())); } } else { for (int i = 0; i > n; i--) { if (column() == 0) { // Have come to the start of the document if (line() == 0) { break; } // Start going back to the end of the last line m_cursor.setLine(line() - 1); // Retrieve the next text range thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line(); return *this; } // Finish going back to the end of the last line m_cursor.setColumn(thisLine->length()); continue; } if (column() > thisLine->length()) { m_cursor.setColumn(column() - 1); } else { m_cursor.setColumn(thisLine->layout()->previousCursorPosition(column())); } } } Q_ASSERT(valid()); return *this; } CalculatingCursor &operator-=(int n) override { return operator+=(-n); } }; void KateViewInternal::moveChar(KateViewInternal::Bias bias, bool sel) { KTextEditor::Cursor c; if (view()->wrapCursor()) { c = WrappingCursor(this, m_cursor) += bias; } else { c = BoundedCursor(this, m_cursor) += bias; } updateSelection(c, sel); updateCursor(c); } void KateViewInternal::cursorPrevChar(bool sel) { if (! view()->wrapCursor() && m_cursor.column() == 0) { return; } moveChar(KateViewInternal::left, sel); } void KateViewInternal::cursorNextChar(bool sel) { moveChar(KateViewInternal::right, sel); } void KateViewInternal::wordPrev(bool sel) { WrappingCursor c(this, m_cursor); // First we skip backwards all space. // Then we look up into which category the current position falls: // 1. a "word" character // 2. a "non-word" character (except space) // 3. the beginning of the line // and skip all preceding characters that fall into this class. // The code assumes that space is never part of the word character class. KateHighlighting *h = doc()->highlight(); if (!c.atEdge(left)) { while (!c.atEdge(left) && doc()->line(c.line())[ c.column() - 1 ].isSpace()) { --c; } } if (c.atEdge(left)) { --c; } else if (h->isInWord(doc()->line(c.line())[ c.column() - 1 ])) { while (!c.atEdge(left) && h->isInWord(doc()->line(c.line())[ c.column() - 1 ])) { --c; } } else { while (!c.atEdge(left) && !h->isInWord(doc()->line(c.line())[ c.column() - 1 ]) // in order to stay symmetric to wordLeft() // we must not skip space preceding a non-word sequence && !doc()->line(c.line())[ c.column() - 1 ].isSpace()) { --c; } } updateSelection(c, sel); updateCursor(c); } void KateViewInternal::wordNext(bool sel) { WrappingCursor c(this, m_cursor); // We look up into which category the current position falls: // 1. a "word" character // 2. a "non-word" character (except space) // 3. the end of the line // and skip all following characters that fall into this class. // If the skipped characters are followed by space, we skip that too. // The code assumes that space is never part of the word character class. KateHighlighting *h = doc()->highlight(); if (c.atEdge(right)) { ++c; } else if (h->isInWord(doc()->line(c.line())[ c.column() ])) { while (!c.atEdge(right) && h->isInWord(doc()->line(c.line())[ c.column() ])) { ++c; } } else { while (!c.atEdge(right) && !h->isInWord(doc()->line(c.line())[ c.column() ]) // we must not skip space, because if that space is followed // by more non-word characters, we would skip them, too && !doc()->line(c.line())[ c.column() ].isSpace()) { ++c; } } while (!c.atEdge(right) && doc()->line(c.line())[ c.column() ].isSpace()) { ++c; } updateSelection(c, sel); updateCursor(c); } void KateViewInternal::moveEdge(KateViewInternal::Bias bias, bool sel) { BoundedCursor c(this, m_cursor); c.toEdge(bias); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::home(bool sel) { if (view()->dynWordWrap() && currentLayout().startCol()) { // Allow us to go to the real start if we're already at the start of the view line if (m_cursor.column() != currentLayout().startCol()) { KTextEditor::Cursor c = currentLayout().start(); updateSelection(c, sel); updateCursor(c); return; } } if (!doc()->config()->smartHome()) { moveEdge(left, sel); return; } Kate::TextLine l = doc()->kateTextLine(m_cursor.line()); if (!l) { return; } KTextEditor::Cursor c = m_cursor; int lc = l->firstChar(); if (lc < 0 || c.column() == lc) { c.setColumn(0); } else { c.setColumn(lc); } updateSelection(c, sel); updateCursor(c, true); } void KateViewInternal::end(bool sel) { KateTextLayout layout = currentLayout(); if (view()->dynWordWrap() && layout.wrap()) { // Allow us to go to the real end if we're already at the end of the view line if (m_cursor.column() < layout.endCol() - 1) { KTextEditor::Cursor c(m_cursor.line(), layout.endCol() - 1); updateSelection(c, sel); updateCursor(c); return; } } if (!doc()->config()->smartHome()) { moveEdge(right, sel); return; } Kate::TextLine l = doc()->kateTextLine(m_cursor.line()); if (!l) { return; } // "Smart End", as requested in bugs #78258 and #106970 if (m_cursor.column() == doc()->lineLength(m_cursor.line())) { KTextEditor::Cursor c = m_cursor; c.setColumn(l->lastChar() + 1); updateSelection(c, sel); updateCursor(c, true); } else { moveEdge(right, sel); } } KateTextLayout KateViewInternal::currentLayout() const { return cache()->textLayout(m_cursor); } KateTextLayout KateViewInternal::previousLayout() const { int currentViewLine = cache()->viewLine(m_cursor); if (currentViewLine) { return cache()->textLayout(m_cursor.line(), currentViewLine - 1); } else { return cache()->textLayout(view()->textFolding().visibleLineToLine(m_displayCursor.line() - 1), -1); } } KateTextLayout KateViewInternal::nextLayout() const { int currentViewLine = cache()->viewLine(m_cursor) + 1; if (currentViewLine >= cache()->line(m_cursor.line())->viewLineCount()) { currentViewLine = 0; return cache()->textLayout(view()->textFolding().visibleLineToLine(m_displayCursor.line() + 1), currentViewLine); } else { return cache()->textLayout(m_cursor.line(), currentViewLine); } } /* * This returns the cursor which is offset by (offset) view lines. * This is the main function which is called by code not specifically dealing with word-wrap. * The opposite conversion (cursor to offset) can be done with cache()->displayViewLine(). * * The cursors involved are virtual cursors (ie. equivalent to m_displayCursor) */ KTextEditor::Cursor KateViewInternal::viewLineOffset(const KTextEditor::Cursor &virtualCursor, int offset, bool keepX) { if (!view()->dynWordWrap()) { KTextEditor::Cursor ret(qMin((int)view()->textFolding().visibleLines() - 1, virtualCursor.line() + offset), 0); if (ret.line() < 0) { ret.setLine(0); } if (keepX) { int realLine = view()->textFolding().visibleLineToLine(ret.line()); KateTextLayout t = cache()->textLayout(realLine, 0); Q_ASSERT(t.isValid()); ret.setColumn(renderer()->xToCursor(t, m_preservedX, !view()->wrapCursor()).column()); } return ret; } KTextEditor::Cursor realCursor = virtualCursor; realCursor.setLine(view()->textFolding().visibleLineToLine(view()->textFolding().lineToVisibleLine(virtualCursor.line()))); int cursorViewLine = cache()->viewLine(realCursor); int currentOffset = 0; int virtualLine = 0; bool forwards = (offset > 0) ? true : false; if (forwards) { currentOffset = cache()->lastViewLine(realCursor.line()) - cursorViewLine; if (offset <= currentOffset) { // the answer is on the same line KateTextLayout thisLine = cache()->textLayout(realCursor.line(), cursorViewLine + offset); Q_ASSERT(thisLine.virtualLine() == (int) view()->textFolding().lineToVisibleLine(virtualCursor.line())); return KTextEditor::Cursor(virtualCursor.line(), thisLine.startCol()); } virtualLine = virtualCursor.line() + 1; } else { offset = -offset; currentOffset = cursorViewLine; if (offset <= currentOffset) { // the answer is on the same line KateTextLayout thisLine = cache()->textLayout(realCursor.line(), cursorViewLine - offset); Q_ASSERT(thisLine.virtualLine() == (int) view()->textFolding().lineToVisibleLine(virtualCursor.line())); return KTextEditor::Cursor(virtualCursor.line(), thisLine.startCol()); } virtualLine = virtualCursor.line() - 1; } currentOffset++; while (virtualLine >= 0 && virtualLine < (int)view()->textFolding().visibleLines()) { int realLine = view()->textFolding().visibleLineToLine(virtualLine); KateLineLayoutPtr thisLine = cache()->line(realLine, virtualLine); if (!thisLine) { break; } for (int i = 0; i < thisLine->viewLineCount(); ++i) { if (offset == currentOffset) { KateTextLayout thisViewLine = thisLine->viewLine(i); if (!forwards) { // We actually want it the other way around int requiredViewLine = cache()->lastViewLine(realLine) - thisViewLine.viewLine(); if (requiredViewLine != thisViewLine.viewLine()) { thisViewLine = thisLine->viewLine(requiredViewLine); } } KTextEditor::Cursor ret(virtualLine, thisViewLine.startCol()); // keep column position if (keepX) { KTextEditor::Cursor realCursor = toRealCursor(virtualCursor); KateTextLayout t = cache()->textLayout(realCursor); // renderer()->cursorToX(t, realCursor, !view()->wrapCursor()); realCursor = renderer()->xToCursor(thisViewLine, m_preservedX, !view()->wrapCursor()); ret.setColumn(realCursor.column()); } return ret; } currentOffset++; } if (forwards) { virtualLine++; } else { virtualLine--; } } // Looks like we were asked for something a bit exotic. // Return the max/min valid position. if (forwards) { return KTextEditor::Cursor(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); } else { return KTextEditor::Cursor(0, 0); } } int KateViewInternal::lineMaxCursorX(const KateTextLayout &range) { if (!view()->wrapCursor() && !range.wrap()) { return INT_MAX; } int maxX = range.endX(); if (maxX && range.wrap()) { QChar lastCharInLine = doc()->kateTextLine(range.line())->at(range.endCol() - 1); maxX -= renderer()->config()->fontMetrics().width(lastCharInLine); } return maxX; } int KateViewInternal::lineMaxCol(const KateTextLayout &range) { int maxCol = range.endCol(); if (maxCol && range.wrap()) { maxCol--; } return maxCol; } void KateViewInternal::cursorUp(bool sel) { if (!sel && view()->completionWidget()->isCompletionActive()) { view()->completionWidget()->cursorUp(); return; } // assert that the display cursor is in visible lines Q_ASSERT(m_displayCursor.line() < view()->textFolding().visibleLines()); /** * move cursor to start of line, if we are at first line! */ if (m_displayCursor.line() == 0 && (!view()->dynWordWrap() || cache()->viewLine(m_cursor) == 0)) { home(sel); return; } m_preserveX = true; KateTextLayout thisLine = currentLayout(); // This is not the first line because that is already simplified out above KateTextLayout pRange = previousLayout(); // Ensure we're in the right spot Q_ASSERT(m_cursor.line() == thisLine.line()); Q_ASSERT(m_cursor.column() >= thisLine.startCol()); Q_ASSERT(!thisLine.wrap() || m_cursor.column() < thisLine.endCol()); KTextEditor::Cursor c = renderer()->xToCursor(pRange, m_preservedX, !view()->wrapCursor()); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::cursorDown(bool sel) { if (!sel && view()->completionWidget()->isCompletionActive()) { view()->completionWidget()->cursorDown(); return; } /** * move cursor to end of line, if we are at last line! */ if ((m_displayCursor.line() >= view()->textFolding().visibleLines() - 1) && (!view()->dynWordWrap() || cache()->viewLine(m_cursor) == cache()->lastViewLine(m_cursor.line()))) { end(sel); return; } m_preserveX = true; KateTextLayout thisLine = currentLayout(); // This is not the last line because that is already simplified out above KateTextLayout nRange = nextLayout(); // Ensure we're in the right spot Q_ASSERT((m_cursor.line() == thisLine.line()) && (m_cursor.column() >= thisLine.startCol()) && (!thisLine.wrap() || m_cursor.column() < thisLine.endCol())); KTextEditor::Cursor c = renderer()->xToCursor(nRange, m_preservedX, !view()->wrapCursor()); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::cursorToMatchingBracket(bool sel) { KTextEditor::Cursor c = findMatchingBracket(); if (c.isValid()) { updateSelection(c, sel); updateCursor(c); } } void KateViewInternal::topOfView(bool sel) { KTextEditor::Cursor c = viewLineOffset(startPos(), m_minLinesVisible); updateSelection(toRealCursor(c), sel); updateCursor(toRealCursor(c)); } void KateViewInternal::bottomOfView(bool sel) { KTextEditor::Cursor c = viewLineOffset(endPos(), -m_minLinesVisible); updateSelection(toRealCursor(c), sel); updateCursor(toRealCursor(c)); } // lines is the offset to scroll by void KateViewInternal::scrollLines(int lines, bool sel) { KTextEditor::Cursor c = viewLineOffset(m_displayCursor, lines, true); // Fix the virtual cursor -> real cursor c.setLine(view()->textFolding().visibleLineToLine(c.line())); updateSelection(c, sel); updateCursor(c); } // This is a bit misleading... it's asking for the view to be scrolled, not the cursor void KateViewInternal::scrollUp() { KTextEditor::Cursor newPos = viewLineOffset(startPos(), -1); scrollPos(newPos); } void KateViewInternal::scrollDown() { KTextEditor::Cursor newPos = viewLineOffset(startPos(), 1); scrollPos(newPos); } void KateViewInternal::setAutoCenterLines(int viewLines, bool updateView) { m_autoCenterLines = viewLines; m_minLinesVisible = qMin(int((linesDisplayed() - 1) / 2), m_autoCenterLines); if (updateView) { KateViewInternal::updateView(); } } void KateViewInternal::pageUp(bool sel, bool half) { if (view()->isCompletionActive()) { view()->completionWidget()->pageUp(); return; } // remember the view line and x pos int viewLine = cache()->displayViewLine(m_displayCursor); bool atTop = startPos().atStartOfDocument(); // Adjust for an auto-centering cursor int lineadj = m_minLinesVisible; int linesToScroll; if (! half) { linesToScroll = -qMax((linesDisplayed() - 1) - lineadj, 0); } else { linesToScroll = -qMax((linesDisplayed() / 2 - 1) - lineadj, 0); } m_preserveX = true; if (!doc()->pageUpDownMovesCursor() && !atTop) { KTextEditor::Cursor newStartPos = viewLineOffset(startPos(), linesToScroll - 1); scrollPos(newStartPos); // put the cursor back approximately where it was KTextEditor::Cursor newPos = toRealCursor(viewLineOffset(newStartPos, viewLine, true)); KateTextLayout newLine = cache()->textLayout(newPos); newPos = renderer()->xToCursor(newLine, m_preservedX, !view()->wrapCursor()); m_preserveX = true; updateSelection(newPos, sel); updateCursor(newPos); } else { scrollLines(linesToScroll, sel); } } void KateViewInternal::pageDown(bool sel, bool half) { if (view()->isCompletionActive()) { view()->completionWidget()->pageDown(); return; } // remember the view line int viewLine = cache()->displayViewLine(m_displayCursor); bool atEnd = startPos() >= m_cachedMaxStartPos; // Adjust for an auto-centering cursor int lineadj = m_minLinesVisible; int linesToScroll; if (! half) { linesToScroll = qMax((linesDisplayed() - 1) - lineadj, 0); } else { linesToScroll = qMax((linesDisplayed() / 2 - 1) - lineadj, 0); } m_preserveX = true; if (!doc()->pageUpDownMovesCursor() && !atEnd) { KTextEditor::Cursor newStartPos = viewLineOffset(startPos(), linesToScroll + 1); scrollPos(newStartPos); // put the cursor back approximately where it was KTextEditor::Cursor newPos = toRealCursor(viewLineOffset(newStartPos, viewLine, true)); KateTextLayout newLine = cache()->textLayout(newPos); newPos = renderer()->xToCursor(newLine, m_preservedX, !view()->wrapCursor()); m_preserveX = true; updateSelection(newPos, sel); updateCursor(newPos); } else { scrollLines(linesToScroll, sel); } } int KateViewInternal::maxLen(int startLine) { Q_ASSERT(!view()->dynWordWrap()); int displayLines = (view()->height() / renderer()->lineHeight()) + 1; int maxLen = 0; for (int z = 0; z < displayLines; z++) { int virtualLine = startLine + z; if (virtualLine < 0 || virtualLine >= (int)view()->textFolding().visibleLines()) { break; } maxLen = qMax(maxLen, cache()->line(view()->textFolding().visibleLineToLine(virtualLine))->width()); } return maxLen; } bool KateViewInternal::columnScrollingPossible() { return !view()->dynWordWrap() && m_columnScroll->isEnabled() && (m_columnScroll->maximum() > 0); } bool KateViewInternal::lineScrollingPossible() { return m_lineScroll->minimum() != m_lineScroll->maximum(); } void KateViewInternal::top(bool sel) { KTextEditor::Cursor newCursor(0, 0); newCursor = renderer()->xToCursor(cache()->textLayout(newCursor), m_preservedX, !view()->wrapCursor()); updateSelection(newCursor, sel); updateCursor(newCursor); } void KateViewInternal::bottom(bool sel) { KTextEditor::Cursor newCursor(doc()->lastLine(), 0); newCursor = renderer()->xToCursor(cache()->textLayout(newCursor), m_preservedX, !view()->wrapCursor()); updateSelection(newCursor, sel); updateCursor(newCursor); } void KateViewInternal::top_home(bool sel) { if (view()->isCompletionActive()) { view()->completionWidget()->top(); return; } KTextEditor::Cursor c(0, 0); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::bottom_end(bool sel) { if (view()->isCompletionActive()) { view()->completionWidget()->bottom(); return; } KTextEditor::Cursor c(doc()->lastLine(), doc()->lineLength(doc()->lastLine())); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::updateSelection(const KTextEditor::Cursor &_newCursor, bool keepSel) { KTextEditor::Cursor newCursor = _newCursor; if (keepSel) { if (!view()->selection() || (m_selectAnchor.line() == -1) //don't kill the selection if we have a persistent selection and //the cursor is inside or at the boundaries of the selected area || (view()->config()->persistentSelection() && !(view()->selectionRange().contains(m_cursor) || view()->selectionRange().boundaryAtCursor(m_cursor)))) { m_selectAnchor = m_cursor; setSelection(KTextEditor::Range(m_cursor, newCursor)); } else { bool doSelect = true; switch (m_selectionMode) { case Word: { // Restore selStartCached if needed. It gets nuked by // viewSelectionChanged if we drag the selection into non-existence, // which can legitimately happen if a shift+DC selection is unable to // set a "proper" (i.e. non-empty) cached selection, e.g. because the // start was on something that isn't a word. Word select mode relies // on the cached selection being set properly, even if it is empty // (i.e. selStartCached == selEndCached). if (!m_selectionCached.isValid()) { m_selectionCached.setStart(m_selectionCached.end()); } int c; if (newCursor > m_selectionCached.start()) { m_selectAnchor = m_selectionCached.start(); Kate::TextLine l = doc()->kateTextLine(newCursor.line()); c = newCursor.column(); if (c > 0 && doc()->highlight()->isInWord(l->at(c - 1))) { for (; c < l->length(); c++) if (!doc()->highlight()->isInWord(l->at(c))) { break; } } newCursor.setColumn(c); } else if (newCursor < m_selectionCached.start()) { m_selectAnchor = m_selectionCached.end(); Kate::TextLine l = doc()->kateTextLine(newCursor.line()); c = newCursor.column(); if (c > 0 && c < doc()->lineLength(newCursor.line()) && doc()->highlight()->isInWord(l->at(c)) && doc()->highlight()->isInWord(l->at(c - 1))) { for (c -= 2; c >= 0; c--) if (!doc()->highlight()->isInWord(l->at(c))) { break; } newCursor.setColumn(c + 1); } } else { doSelect = false; } } break; case Line: if (!m_selectionCached.isValid()) { m_selectionCached = KTextEditor::Range(endLine(), 0, endLine(), 0); } if (newCursor.line() > m_selectionCached.start().line()) { if (newCursor.line() + 1 >= doc()->lines()) { newCursor.setColumn(doc()->line(newCursor.line()).length()); } else { newCursor.setPosition(newCursor.line() + 1, 0); } // Grow to include the entire line m_selectAnchor = m_selectionCached.start(); m_selectAnchor.setColumn(0); } else if (newCursor.line() < m_selectionCached.start().line()) { newCursor.setColumn(0); // Grow to include entire line m_selectAnchor = m_selectionCached.end(); if (m_selectAnchor.column() > 0) { if (m_selectAnchor.line() + 1 >= doc()->lines()) { m_selectAnchor.setColumn(doc()->line(newCursor.line()).length()); } else { m_selectAnchor.setPosition(m_selectAnchor.line() + 1, 0); } } } else { // same line, ignore doSelect = false; } break; case Mouse: { if (!m_selectionCached.isValid()) { break; } if (newCursor > m_selectionCached.end()) { m_selectAnchor = m_selectionCached.start(); } else if (newCursor < m_selectionCached.start()) { m_selectAnchor = m_selectionCached.end(); } else { doSelect = false; } } break; default: /* nothing special to do */; } if (doSelect) { setSelection(KTextEditor::Range(m_selectAnchor, newCursor)); } else if (m_selectionCached.isValid()) { // we have a cached selection, so we restore that setSelection(m_selectionCached); } } m_selChangedByUser = true; } else if (!view()->config()->persistentSelection()) { view()->clearSelection(); m_selectionCached = KTextEditor::Range::invalid(); m_selectAnchor = KTextEditor::Cursor::invalid(); } #ifndef QT_NO_ACCESSIBILITY // FIXME KF5 // QAccessibleTextSelectionEvent ev(this, /* selection start, selection end*/); // QAccessible::updateAccessibility(&ev); #endif } void KateViewInternal::setSelection(const KTextEditor::Range &range) { disconnect(m_view, SIGNAL(selectionChanged(KTextEditor::View*)), this, SLOT(viewSelectionChanged())); view()->setSelection(range); connect(m_view, SIGNAL(selectionChanged(KTextEditor::View*)), this, SLOT(viewSelectionChanged())); } void KateViewInternal::moveCursorToSelectionEdge() { if (!view()->selection()) { return; } int tmp = m_minLinesVisible; m_minLinesVisible = 0; if (view()->selectionRange().start() < m_selectAnchor) { updateCursor(view()->selectionRange().start()); } else { updateCursor(view()->selectionRange().end()); } m_minLinesVisible = tmp; } void KateViewInternal::updateCursor(const KTextEditor::Cursor &newCursor, bool force, bool center, bool calledExternally) { if (!force && (m_cursor.toCursor() == newCursor)) { m_displayCursor = toVirtualCursor(newCursor); if (!m_madeVisible && m_view == doc()->activeView()) { // unfold if required view()->textFolding().ensureLineIsVisible(newCursor.line()); makeVisible(m_displayCursor, m_displayCursor.column(), false, center, calledExternally); } return; } if (m_cursor.line() != newCursor.line()) { m_leftBorder->updateForCursorLineChange(); } // unfold if required view()->textFolding().ensureLineIsVisible(newCursor.line()); KTextEditor::Cursor oldDisplayCursor = m_displayCursor; m_displayCursor = toVirtualCursor(newCursor); m_cursor.setPosition(newCursor); if (m_view == doc()->activeView()) { makeVisible(m_displayCursor, m_displayCursor.column(), false, center, calledExternally); } updateBracketMarks(); // It's efficient enough to just tag them both without checking to see if they're on the same view line /* kdDebug()<<"oldDisplayCursor:"< 0) { m_cursorTimer.start(QApplication::cursorFlashTime() / 2); } renderer()->setDrawCaret(true); } // Remember the maximum X position if requested if (m_preserveX) { m_preserveX = false; } else { m_preservedX = renderer()->cursorToX(cache()->textLayout(m_cursor), m_cursor, !view()->wrapCursor()); } //qCDebug(LOG_KTE) << "m_preservedX: " << m_preservedX << " (was "<< oldmaxx << "), m_cursorX: " << m_cursorX; //qCDebug(LOG_KTE) << "Cursor now located at real " << cursor.line << "," << cursor.col << ", virtual " << m_displayCursor.line << ", " << m_displayCursor.col << "; Top is " << startLine() << ", " << startPos().col; cursorMoved(); updateDirty(); //paintText(0, 0, width(), height(), true); emit view()->cursorPositionChanged(m_view, m_cursor); } void KateViewInternal::updateBracketMarkAttributes() { KTextEditor::Attribute::Ptr bracketFill = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); bracketFill->setBackground(view()->m_renderer->config()->highlightedBracketColor()); bracketFill->setBackgroundFillWhitespace(false); if (QFontInfo(renderer()->currentFont()).fixedPitch()) { // make font bold only for fixed fonts, otherwise text jumps around bracketFill->setFontBold(); } m_bmStart->setAttribute(bracketFill); m_bmEnd->setAttribute(bracketFill); if (view()->m_renderer->config()->showWholeBracketExpression()) { KTextEditor::Attribute::Ptr expressionFill = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); expressionFill->setBackground(view()->m_renderer->config()->highlightedBracketColor()); expressionFill->setBackgroundFillWhitespace(false); m_bm->setAttribute(expressionFill); } else { m_bm->setAttribute(KTextEditor::Attribute::Ptr(new KTextEditor::Attribute())); } } void KateViewInternal::updateBracketMarks() { // add some limit to this, this is really endless on big files without limit const int maxLines = 5000; const KTextEditor::Range newRange = doc()->findMatchingBracket(m_cursor, maxLines); // new range valid, then set ranges to it if (newRange.isValid()) { if (m_bm->toRange() == newRange) { return; } // modify full range m_bm->setRange(newRange); // modify start and end ranges m_bmStart->setRange(KTextEditor::Range(m_bm->start(), KTextEditor::Cursor(m_bm->start().line(), m_bm->start().column() + 1))); m_bmEnd->setRange(KTextEditor::Range(m_bm->end(), KTextEditor::Cursor(m_bm->end().line(), m_bm->end().column() + 1))); // flash matching bracket if (!renderer()->config()->animateBracketMatching()) { return; } const KTextEditor::Cursor flashPos = (m_cursor == m_bmStart->start() || m_cursor == m_bmStart->end()) ? m_bmEnd->start() : m_bm->start(); if (flashPos != m_bmLastFlashPos->toCursor()) { m_bmLastFlashPos->setPosition(flashPos); KTextEditor::Attribute::Ptr attribute = doc()->attributeAt(flashPos); attribute->setBackground(view()->m_renderer->config()->highlightedBracketColor()); attribute->setFontBold(m_bmStart->attribute()->fontBold()); flashChar(flashPos, attribute); } return; } // new range was invalid m_bm->setRange(KTextEditor::Range::invalid()); m_bmStart->setRange(KTextEditor::Range::invalid()); m_bmEnd->setRange(KTextEditor::Range::invalid()); m_bmLastFlashPos->setPosition(KTextEditor::Cursor::invalid()); } bool KateViewInternal::tagLine(const KTextEditor::Cursor &virtualCursor) { // FIXME may be a more efficient way for this if ((int)view()->textFolding().visibleLineToLine(virtualCursor.line()) > doc()->lastLine()) { return false; } // End FIXME int viewLine = cache()->displayViewLine(virtualCursor, true); if (viewLine >= 0 && viewLine < cache()->viewCacheLineCount()) { cache()->viewLine(viewLine).setDirty(); // tag one line more because of overlapping things like _, bug 335079 if (viewLine+1 < cache()->viewCacheLineCount()) { cache()->viewLine(viewLine+1).setDirty(); } m_leftBorder->update(0, lineToY(viewLine), m_leftBorder->width(), renderer()->lineHeight()); return true; } return false; } bool KateViewInternal::tagLines(int start, int end, bool realLines) { return tagLines(KTextEditor::Cursor(start, 0), KTextEditor::Cursor(end, -1), realLines); } bool KateViewInternal::tagLines(KTextEditor::Cursor start, KTextEditor::Cursor end, bool realCursors) { if (realCursors) { cache()->relayoutLines(start.line(), end.line()); //qCDebug(LOG_KTE)<<"realLines is true"; start = toVirtualCursor(start); end = toVirtualCursor(end); } else { cache()->relayoutLines(toRealCursor(start).line(), toRealCursor(end).line()); } if (end.line() < startLine()) { //qCDebug(LOG_KTE)<<"end endLine(), but cache may not be valid when checking, so use a // less optimal but still adequate approximation (potential overestimation but minimal performance difference) if (start.line() > startLine() + cache()->viewCacheLineCount()) { //qCDebug(LOG_KTE)<<"start> endLine"<updateViewCache(startPos()); //qCDebug(LOG_KTE) << "tagLines( [" << start << "], [" << end << "] )"; bool ret = false; for (int z = 0; z < cache()->viewCacheLineCount(); z++) { KateTextLayout &line = cache()->viewLine(z); if ((line.virtualLine() > start.line() || (line.virtualLine() == start.line() && line.endCol() >= start.column() && start.column() != -1)) && (line.virtualLine() < end.line() || (line.virtualLine() == end.line() && (line.startCol() <= end.column() || end.column() == -1)))) { ret = true; break; //qCDebug(LOG_KTE) << "Tagged line " << line.line(); } } if (!view()->dynWordWrap()) { int y = lineToY(start.line()); // FIXME is this enough for when multiple lines are deleted int h = (end.line() - start.line() + 2) * renderer()->lineHeight(); if (end.line() >= view()->textFolding().visibleLines() - 1) { h = height(); } m_leftBorder->update(0, y, m_leftBorder->width(), h); } else { // FIXME Do we get enough good info in editRemoveText to optimize this more? //bool justTagged = false; for (int z = 0; z < cache()->viewCacheLineCount(); z++) { KateTextLayout &line = cache()->viewLine(z); if (!line.isValid() || ((line.virtualLine() > start.line() || (line.virtualLine() == start.line() && line.endCol() >= start.column() && start.column() != -1)) && (line.virtualLine() < end.line() || (line.virtualLine() == end.line() && (line.startCol() <= end.column() || end.column() == -1))))) { //justTagged = true; m_leftBorder->update(0, z * renderer()->lineHeight(), m_leftBorder->width(), m_leftBorder->height()); break; } /*else if (justTagged) { justTagged = false; leftBorder->update (0, z * doc()->viewFont.fontHeight, leftBorder->width(), doc()->viewFont.fontHeight); break; }*/ } } return ret; } bool KateViewInternal::tagRange(const KTextEditor::Range &range, bool realCursors) { return tagLines(range.start(), range.end(), realCursors); } void KateViewInternal::tagAll() { // clear the cache... cache()->clear(); m_leftBorder->updateFont(); m_leftBorder->update(); } void KateViewInternal::paintCursor() { if (tagLine(m_displayCursor)) { updateDirty(); //paintText (0,0,width(), height(), true); } } // Point in content coordinates void KateViewInternal::placeCursor(const QPoint &p, bool keepSelection, bool updateSelection) { KateTextLayout thisLine = yToKateTextLayout(p.y()); KTextEditor::Cursor c; if (!thisLine.isValid()) { // probably user clicked below the last line -> use the last line thisLine = cache()->textLayout(doc()->lines() - 1, -1); } c = renderer()->xToCursor(thisLine, startX() + p.x(), !view()->wrapCursor()); if (c.line() < 0 || c.line() >= doc()->lines()) { return; } if (updateSelection) { KateViewInternal::updateSelection(c, keepSelection); } int tmp = m_minLinesVisible; m_minLinesVisible = 0; updateCursor(c); m_minLinesVisible = tmp; if (updateSelection && keepSelection) { moveCursorToSelectionEdge(); } } // Point in content coordinates bool KateViewInternal::isTargetSelected(const QPoint &p) { const KateTextLayout &thisLine = yToKateTextLayout(p.y()); if (!thisLine.isValid()) { return false; } return view()->cursorSelected(renderer()->xToCursor(thisLine, startX() + p.x(), !view()->wrapCursor())); } //BEGIN EVENT HANDLING STUFF bool KateViewInternal::eventFilter(QObject *obj, QEvent *e) { switch (e->type()) { case QEvent::ChildAdded: case QEvent::ChildRemoved: { QChildEvent *c = static_cast(e); if (c->added()) { c->child()->installEventFilter(this); /*foreach (QWidget* child, c->child()->findChildren()) child->installEventFilter(this);*/ } else if (c->removed()) { c->child()->removeEventFilter(this); /*foreach (QWidget* child, c->child()->findChildren()) child->removeEventFilter(this);*/ } } break; case QEvent::ShortcutOverride: { QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { if (view()->isCompletionActive()) { view()->abortCompletion(); k->accept(); //qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "aborting completion"; return true; } else if (!view()->bottomViewBar()->hiddenOrPermanent()) { view()->bottomViewBar()->hideCurrentBarWidget(); k->accept(); //qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "closing view bar"; return true; } else if (!view()->config()->persistentSelection() && view()->selection()) { m_currentInputMode->clearSelection(); k->accept(); //qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "clearing selection"; return true; } } if (m_currentInputMode->stealKey(k)) { k->accept(); return true; } } break; case QEvent::KeyPress: { QKeyEvent *k = static_cast(e); // Override all other single key shortcuts which do not use a modifier other than Shift if (obj == this && (!k->modifiers() || k->modifiers() == Qt::ShiftModifier)) { keyPressEvent(k); if (k->isAccepted()) { //qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "using keystroke"; return true; } } //qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "ignoring"; } break; case QEvent::DragMove: { QPoint currentPoint = ((QDragMoveEvent *) e)->pos(); QRect doNotScrollRegion(s_scrollMargin, s_scrollMargin, width() - s_scrollMargin * 2, height() - s_scrollMargin * 2); if (!doNotScrollRegion.contains(currentPoint)) { startDragScroll(); // Keep sending move events ((QDragMoveEvent *)e)->accept(QRect(0, 0, 0, 0)); } dragMoveEvent((QDragMoveEvent *)e); } break; case QEvent::DragLeave: // happens only when pressing ESC while dragging stopDragScroll(); break; default: break; } return QWidget::eventFilter(obj, e); } void KateViewInternal::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Left && e->modifiers() == Qt::AltModifier) { view()->emitNavigateLeft(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Right && e->modifiers() == Qt::AltModifier) { view()->emitNavigateRight(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Up && e->modifiers() == Qt::AltModifier) { view()->emitNavigateUp(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Down && e->modifiers() == Qt::AltModifier) { view()->emitNavigateDown(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Return && e->modifiers() == Qt::AltModifier) { view()->emitNavigateAccept(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Backspace && e->modifiers() == Qt::AltModifier) { view()->emitNavigateBack(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Alt && view()->completionWidget()->isCompletionActive()) { m_completionItemExpanded = view()->completionWidget()->toggleExpanded(true); view()->completionWidget()->resetHadNavigation(); m_altDownTime.start(); } // Note: AND'ing with is a quick hack to fix Key_Enter const int key = e->key() | (e->modifiers() & Qt::ShiftModifier); if (m_currentInputMode->keyPress(e)) { return; } if (!doc()->isReadWrite()) { e->ignore(); return; } if ((key == Qt::Key_Return) || (key == Qt::Key_Enter) || (key == Qt::SHIFT + Qt::Key_Return) || (key == Qt::SHIFT + Qt::Key_Enter)) { view()->keyReturn(); e->accept(); return; } if (key == Qt::Key_Backspace || key == Qt::SHIFT + Qt::Key_Backspace) { //view()->backspace(); e->accept(); return; } if (key == Qt::Key_Tab || key == Qt::SHIFT + Qt::Key_Backtab || key == Qt::Key_Backtab) { if (view()->completionWidget()->isCompletionActive()) { e->accept(); view()->completionWidget()->tab(key != Qt::Key_Tab); return; } if (key == Qt::Key_Tab) { uint tabHandling = doc()->config()->tabHandling(); // convert tabSmart into tabInsertsTab or tabIndents: if (tabHandling == KateDocumentConfig::tabSmart) { // multiple lines selected if (view()->selection() && !view()->selectionRange().onSingleLine()) { tabHandling = KateDocumentConfig::tabIndents; } // otherwise: take look at cursor position else { // if the cursor is at or before the first non-space character // or on an empty line, // Tab indents, otherwise it inserts a tab character. Kate::TextLine line = doc()->kateTextLine(m_cursor.line()); int first = line->firstChar(); if (first < 0 || m_cursor.column() <= first) { tabHandling = KateDocumentConfig::tabIndents; } else { tabHandling = KateDocumentConfig::tabInsertsTab; } } } if (tabHandling == KateDocumentConfig::tabInsertsTab) { doc()->typeChars(m_view, QStringLiteral("\t")); } else { doc()->indent(view()->selection() ? view()->selectionRange() : KTextEditor::Range(m_cursor.line(), 0, m_cursor.line(), 0), 1); } e->accept(); return; } else if (doc()->config()->tabHandling() != KateDocumentConfig::tabInsertsTab) { // key == Qt::SHIFT+Qt::Key_Backtab || key == Qt::Key_Backtab doc()->indent(view()->selection() ? view()->selectionRange() : KTextEditor::Range(m_cursor.line(), 0, m_cursor.line(), 0), -1); e->accept(); return; } } if (!(e->modifiers() & Qt::ControlModifier) && !e->text().isEmpty() && doc()->typeChars(m_view, e->text())) { e->accept(); return; } // allow composition of AltGr + (q|2|3) on windows static const int altGR = Qt::ControlModifier | Qt::AltModifier; if ((e->modifiers() & altGR) == altGR && !e->text().isEmpty() && doc()->typeChars(m_view, e->text())) { e->accept(); return; } e->ignore(); } void KateViewInternal::keyReleaseEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Alt && view()->completionWidget()->isCompletionActive() && ((m_completionItemExpanded && (view()->completionWidget()->hadNavigation() || m_altDownTime.elapsed() > 300)) || (!m_completionItemExpanded && !view()->completionWidget()->hadNavigation()))) { view()->completionWidget()->toggleExpanded(false, true); } if ((e->modifiers() & Qt::SHIFT) == Qt::SHIFT) { m_shiftKeyPressed = true; } else { if (m_shiftKeyPressed) { m_shiftKeyPressed = false; if (m_selChangedByUser) { if (view()->selection()) { QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); } m_selChangedByUser = false; } } } e->ignore(); return; } void KateViewInternal::contextMenuEvent(QContextMenuEvent *e) { // try to show popup menu QPoint p = e->pos(); if (e->reason() == QContextMenuEvent::Keyboard) { makeVisible(m_displayCursor, 0); p = cursorCoordinates(false); p.rx() -= startX(); } else if (! view()->selection() || view()->config()->persistentSelection()) { placeCursor(e->pos()); } // popup is a qguardedptr now if (view()->contextMenu()) { view()->spellingMenu()->setUseMouseForMisspelledRange((e->reason() == QContextMenuEvent::Mouse)); view()->contextMenu()->popup(mapToGlobal(p)); e->accept(); } } void KateViewInternal::mousePressEvent(QMouseEvent *e) { // was an inline note clicked? const auto noteData = inlineNoteAt(e->globalPos()); const KTextEditor::InlineNote note(noteData); if (note.position().isValid()) { note.provider()->inlineNoteActivated(noteData, e->button(), e->globalPos()); return; } // no -- continue with normal handling switch (e->button()) { case Qt::LeftButton: m_selChangedByUser = false; if (m_possibleTripleClick) { m_possibleTripleClick = false; m_selectionMode = Line; if (e->modifiers() & Qt::ShiftModifier) { updateSelection(m_cursor, true); } else { view()->selectLine(m_cursor); if (view()->selection()) { m_selectAnchor = view()->selectionRange().start(); } } if (view()->selection()) { QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); } // Keep the line at the select anchor selected during further // mouse selection if (m_selectAnchor.line() > view()->selectionRange().start().line()) { // Preserve the last selected line if (m_selectAnchor == view()->selectionRange().end() && m_selectAnchor.column() == 0) { m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line() - 1, 0)); } else { m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line(), 0)); } m_selectionCached.setEnd(view()->selectionRange().end()); } else { // Preserve the first selected line m_selectionCached.setStart(view()->selectionRange().start()); if (view()->selectionRange().end().line() > view()->selectionRange().start().line()) { m_selectionCached.setEnd(KTextEditor::Cursor(view()->selectionRange().start().line() + 1, 0)); } else { m_selectionCached.setEnd(view()->selectionRange().end()); } } moveCursorToSelectionEdge(); m_scrollX = 0; m_scrollY = 0; m_scrollTimer.start(50); e->accept(); return; } else if (m_selectionMode == Default) { m_selectionMode = Mouse; } // request the software keyboard, if any if (e->button() == Qt::LeftButton && qApp->autoSipEnabled()) { QStyle::RequestSoftwareInputPanel behavior = QStyle::RequestSoftwareInputPanel(style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { QEvent event(QEvent::RequestSoftwareInputPanel); QApplication::sendEvent(this, &event); } } if (e->modifiers() & Qt::ShiftModifier) { if (!m_selectAnchor.isValid()) { m_selectAnchor = m_cursor; } } else { m_selectionCached = KTextEditor::Range::invalid(); } if (!(e->modifiers() & Qt::ShiftModifier) && isTargetSelected(e->pos())) { m_dragInfo.state = diPending; m_dragInfo.start = e->pos(); } else { m_dragInfo.state = diNone; if (e->modifiers() & Qt::ShiftModifier) { placeCursor(e->pos(), true, false); if (m_selectionCached.start().isValid()) { if (m_cursor.toCursor() < m_selectionCached.start()) { m_selectAnchor = m_selectionCached.end(); } else { m_selectAnchor = m_selectionCached.start(); } } setSelection(KTextEditor::Range(m_selectAnchor, m_cursor)); } else { placeCursor(e->pos()); } m_scrollX = 0; m_scrollY = 0; m_scrollTimer.start(50); } e->accept(); break; default: e->ignore(); break; } } void KateViewInternal::mouseDoubleClickEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { m_selectionMode = Word; if (e->modifiers() & Qt::ShiftModifier) { // Now select the word under the select anchor int cs, ce; Kate::TextLine l = doc()->kateTextLine(m_selectAnchor.line()); ce = m_selectAnchor.column(); if (ce > 0 && doc()->highlight()->isInWord(l->at(ce))) { for (; ce < l->length(); ce++) if (!doc()->highlight()->isInWord(l->at(ce))) { break; } } cs = m_selectAnchor.column() - 1; if (cs < doc()->lineLength(m_selectAnchor.line()) && doc()->highlight()->isInWord(l->at(cs))) { for (cs--; cs >= 0; cs--) if (!doc()->highlight()->isInWord(l->at(cs))) { break; } } // ...and keep it selected if (cs + 1 < ce) { m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line(), cs + 1)); m_selectionCached.setEnd(KTextEditor::Cursor(m_selectAnchor.line(), ce)); } else { m_selectionCached.setStart(m_selectAnchor); m_selectionCached.setEnd(m_selectAnchor); } // Now word select to the mouse cursor placeCursor(e->pos(), true); } else { // first clear the selection, otherwise we run into bug #106402 // ...and set the cursor position, for the same reason (otherwise there // are *other* idiosyncrasies we can't fix without reintroducing said // bug) // Parameters: don't redraw, and don't emit selectionChanged signal yet view()->clearSelection(false, false); placeCursor(e->pos()); view()->selectWord(m_cursor); cursorToMatchingBracket(true); if (view()->selection()) { m_selectAnchor = view()->selectionRange().start(); m_selectionCached = view()->selectionRange(); } else { m_selectAnchor = m_cursor; m_selectionCached = KTextEditor::Range(m_cursor, m_cursor); } } // Move cursor to end (or beginning) of selected word #ifndef Q_OS_MACOS if (view()->selection()) { QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); } #endif moveCursorToSelectionEdge(); m_possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, SLOT(tripleClickTimeout())); m_scrollX = 0; m_scrollY = 0; m_scrollTimer.start(50); e->accept(); } else { e->ignore(); } } void KateViewInternal::tripleClickTimeout() { m_possibleTripleClick = false; } void KateViewInternal::beginSelectLine(const QPoint &pos) { placeCursor(pos); m_possibleTripleClick = true; // set so subsequent mousePressEvent will select line } void KateViewInternal::mouseReleaseEvent(QMouseEvent *e) { switch (e->button()) { case Qt::LeftButton: m_selectionMode = Default; // m_selectionCached.start().setLine( -1 ); if (m_selChangedByUser) { if (view()->selection()) { QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); } moveCursorToSelectionEdge(); m_selChangedByUser = false; } if (m_dragInfo.state == diPending) { placeCursor(e->pos(), e->modifiers() & Qt::ShiftModifier); } else if (m_dragInfo.state == diNone) { m_scrollTimer.stop(); } m_dragInfo.state = diNone; e->accept(); break; case Qt::MidButton: if (!view()->config()->mousePasteAtCursorPosition()) { placeCursor(e->pos()); } if (doc()->isReadWrite()) { QString clipboard = QApplication::clipboard()->text(QClipboard::Selection); view()->paste(&clipboard); } e->accept(); break; default: e->ignore(); break; } } void KateViewInternal::leaveEvent(QEvent *) { m_textHintTimer.stop(); // fix bug 194452, scrolling keeps going if you scroll via mouse drag and press and other mouse // button outside the view area if (m_dragInfo.state == diNone) { m_scrollTimer.stop(); } } KTextEditor::Cursor KateViewInternal::coordinatesToCursor(const QPoint &_coord, bool includeBorder) const { QPoint coord(_coord); KTextEditor::Cursor ret = KTextEditor::Cursor::invalid(); if (includeBorder) { coord.rx() -= m_leftBorder->width(); } coord.rx() += startX(); const KateTextLayout &thisLine = yToKateTextLayout(coord.y()); if (thisLine.isValid()) { ret = renderer()->xToCursor(thisLine, coord.x(), !view()->wrapCursor()); } if (ret.column() > view()->document()->lineLength(ret.line())) { // The cursor is beyond the end of the line; in that case the renderer // gives the index of the character behind the last one. return KTextEditor::Cursor::invalid(); } return ret; } void KateViewInternal::mouseMoveEvent(QMouseEvent *e) { KTextEditor::Cursor newPosition = coordinatesToCursor(e->pos(), false); if (newPosition != m_mouse) { m_mouse = newPosition; mouseMoved(); } if (e->buttons() == Qt::NoButton) { const auto noteData = inlineNoteAt(e->globalPos()); const KTextEditor::InlineNote note(noteData); const KTextEditor::InlineNote activeNote(m_activeInlineNote); if (note.position().isValid()) { if (!activeNote.position().isValid()) { // no active note -- focus in note.provider()->inlineNoteFocusInEvent(note, e->globalPos()); m_activeInlineNote = noteData; } else { note.provider()->inlineNoteMouseMoveEvent(note, e->globalPos()); } // the note might change its appearance in result to the event tagLines(note.position(), note.position(), true); } else if (activeNote.position().isValid()) { activeNote.provider()->inlineNoteFocusOutEvent(activeNote); tagLines(activeNote.position(), activeNote.position(), true); m_activeInlineNote = {}; } } if (e->buttons() & Qt::LeftButton) { if (m_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet // if the mouse has moved sufficiently, we will confirm QPoint p(e->pos() - m_dragInfo.start); // we've left the drag square, we can start a real drag operation now if (p.manhattanLength() > QApplication::startDragDistance()) { doDrag(); } return; } else if (m_dragInfo.state == diDragging) { // Don't do anything after a canceled drag until the user lets go of // the mouse button! return; } m_mouseX = e->x(); m_mouseY = e->y(); m_scrollX = 0; m_scrollY = 0; int d = renderer()->lineHeight(); if (m_mouseX < 0) { m_scrollX = -d; } if (m_mouseX > width()) { m_scrollX = d; } if (m_mouseY < 0) { m_mouseY = 0; m_scrollY = -d; } if (m_mouseY > height()) { m_mouseY = height(); m_scrollY = d; } placeCursor(QPoint(m_mouseX, m_mouseY), true); } else { if (isTargetSelected(e->pos())) { // mouse is over selected text. indicate that the text is draggable by setting // the arrow cursor as other Qt text editing widgets do if (m_mouseCursor != Qt::ArrowCursor) { m_mouseCursor = Qt::ArrowCursor; setCursor(m_mouseCursor); } } else { // normal text cursor if (m_mouseCursor != Qt::IBeamCursor) { m_mouseCursor = Qt::IBeamCursor; setCursor(m_mouseCursor); } } //We need to check whether the mouse position is actually within the widget, //because other widgets like the icon border forward their events to this, //and we will create invalid text hint requests if we don't check if (textHintsEnabled() && geometry().contains(parentWidget()->mapFromGlobal(e->globalPos()))) { if (QToolTip::isVisible()) { QToolTip::hideText(); } m_textHintTimer.start(m_textHintDelay); m_textHintPos = e->pos(); } } } void KateViewInternal::updateDirty() { const int h = renderer()->lineHeight(); int currentRectStart = -1; int currentRectEnd = -1; QRegion updateRegion; { for (int i = 0; i < cache()->viewCacheLineCount(); ++i) { if (cache()->viewLine(i).isDirty()) { if (currentRectStart == -1) { currentRectStart = h * i; currentRectEnd = h; } else { currentRectEnd += h; } } else if (currentRectStart != -1) { updateRegion += QRect(0, currentRectStart, width(), currentRectEnd); currentRectStart = -1; currentRectEnd = -1; } } } if (currentRectStart != -1) { updateRegion += QRect(0, currentRectStart, width(), currentRectEnd); } if (!updateRegion.isEmpty()) { if (debugPainting) { qCDebug(LOG_KTE) << "Update dirty region " << updateRegion; } update(updateRegion); } } void KateViewInternal::hideEvent(QHideEvent *e) { Q_UNUSED(e); if (view()->isCompletionActive()) { view()->completionWidget()->abortCompletion(); } } void KateViewInternal::paintEvent(QPaintEvent *e) { if (debugPainting) { qCDebug(LOG_KTE) << "GOT PAINT EVENT: Region" << e->region(); } const QRect &unionRect = e->rect(); int xStart = startX() + unionRect.x(); int xEnd = xStart + unionRect.width(); uint h = renderer()->lineHeight(); uint startz = (unionRect.y() / h); uint endz = startz + 1 + (unionRect.height() / h); uint lineRangesSize = cache()->viewCacheLineCount(); const KTextEditor::Cursor pos = m_cursor; QPainter paint(this); paint.setRenderHints(QPainter::Antialiasing); paint.save(); renderer()->setCaretStyle(m_currentInputMode->caretStyle()); renderer()->setShowTabs(doc()->config()->showTabs()); renderer()->setShowSpaces(doc()->config()->showSpaces()); renderer()->updateMarkerSize(); /** * paint line by line * this includes parts that span areas without real lines * translate to first line to paint */ paint.translate(unionRect.x(), startz * h); for (uint z = startz; z <= endz; z++) { /** * paint regions without lines mapped to */ if ((z >= lineRangesSize) || (cache()->viewLine(z).line() == -1)) { if (!(z >= lineRangesSize)) { cache()->viewLine(z).setDirty(false); } paint.fillRect(0, 0, unionRect.width(), h, renderer()->config()->backgroundColor()); } /** * paint text lines */ else { /** * If viewLine() returns non-zero, then a document line was split * in several visual lines, and we're trying to paint visual line * that is not the first. In that case, this line was already * painted previously, since KateRenderer::paintTextLine paints * all visual lines. * * Except if we're at the start of the region that needs to * be painted -- when no previous calls to paintTextLine were made. */ KateTextLayout &thisLine = cache()->viewLine(z); if (!thisLine.viewLine() || z == startz) { /** * paint our line * set clipping region to only paint the relevant parts */ paint.save(); paint.translate(QPoint(0, h * - thisLine.viewLine())); paint.setClipRect(QRect(0, 0, unionRect.width(), h * thisLine.kateLineLayout()->viewLineCount())); renderer()->paintTextLine(paint, thisLine.kateLineLayout(), xStart, xEnd, &pos); paint.restore(); /** * line painted, reset and state + mark line as non-dirty */ thisLine.setDirty(false); } } /** * translate to next line */ paint.translate(0, h); } paint.restore(); if (m_textAnimation) { m_textAnimation->draw(paint); } } void KateViewInternal::resizeEvent(QResizeEvent *e) { bool expandedHorizontally = width() > e->oldSize().width(); bool expandedVertically = height() > e->oldSize().height(); bool heightChanged = height() != e->oldSize().height(); m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height()); m_madeVisible = false; if (heightChanged) { setAutoCenterLines(m_autoCenterLines, false); m_cachedMaxStartPos.setPosition(-1, -1); } if (view()->dynWordWrap()) { bool dirtied = false; for (int i = 0; i < cache()->viewCacheLineCount(); i++) { // find the first dirty line // the word wrap updateView algorithm is forced to check all lines after a dirty one KateTextLayout viewLine = cache()->viewLine(i); if (viewLine.wrap() || viewLine.isRightToLeft() || viewLine.width() > width()) { dirtied = true; viewLine.setDirty(); break; } } if (dirtied || heightChanged) { updateView(true); m_leftBorder->update(); } } else { updateView(); if (expandedHorizontally && startX() > 0) { scrollColumns(startX() - (width() - e->oldSize().width())); } } if (width() < e->oldSize().width() && !view()->wrapCursor()) { // May have to restrain cursor to new smaller width... if (m_cursor.column() > doc()->lineLength(m_cursor.line())) { KateTextLayout thisLine = currentLayout(); KTextEditor::Cursor newCursor(m_cursor.line(), thisLine.endCol() + ((width() - thisLine.xOffset() - (thisLine.width() - startX())) / renderer()->spaceWidth()) - 1); if (newCursor.column() < m_cursor.column()) { updateCursor(newCursor); } } } if (expandedVertically) { KTextEditor::Cursor max = maxStartPos(); if (startPos() > max) { scrollPos(max); return; // already fired displayRangeChanged } } emit view()->displayRangeChanged(m_view); } void KateViewInternal::scrollTimeout() { if (m_scrollX || m_scrollY) { scrollLines(startLine() + (m_scrollY / (int) renderer()->lineHeight())); placeCursor(QPoint(m_mouseX, m_mouseY), true); } } void KateViewInternal::cursorTimeout() { if (!debugPainting && m_currentInputMode->blinkCaret()) { renderer()->setDrawCaret(!renderer()->drawCaret()); paintCursor(); } } void KateViewInternal::textHintTimeout() { m_textHintTimer.stop(); KTextEditor::Cursor c = coordinatesToCursor(m_textHintPos, false); if (!c.isValid()) { return; } QStringList textHints; foreach(KTextEditor::TextHintProvider * const p, m_textHintProviders) { const QString hint = p->textHint(m_view, c); if (!hint.isEmpty()) { textHints.append(hint); } } if (!textHints.isEmpty()) { qCDebug(LOG_KTE) << "Hint text: " << textHints; QString hint; foreach(const QString & str, textHints) { hint += QStringLiteral("

%1

").arg(str); } QPoint pos(startX() + m_textHintPos.x(), m_textHintPos.y()); QToolTip::showText(mapToGlobal(pos), hint); } } void KateViewInternal::focusInEvent(QFocusEvent *) { if (QApplication::cursorFlashTime() > 0) { m_cursorTimer.start(QApplication::cursorFlashTime() / 2); } paintCursor(); doc()->setActiveView(m_view); // this will handle focus stuff in kateview view()->slotGotFocus(); } void KateViewInternal::focusOutEvent(QFocusEvent *) { //if (view()->isCompletionActive()) //view()->abortCompletion(); m_cursorTimer.stop(); view()->renderer()->setDrawCaret(true); paintCursor(); m_textHintTimer.stop(); view()->slotLostFocus(); } void KateViewInternal::doDrag() { m_dragInfo.state = diDragging; m_dragInfo.dragObject = new QDrag(this); QMimeData *mimeData = new QMimeData(); mimeData->setText(view()->selectionText()); m_dragInfo.dragObject->setMimeData(mimeData); m_dragInfo.dragObject->start(Qt::MoveAction); } void KateViewInternal::dragEnterEvent(QDragEnterEvent *event) { if (event->source() == this) { event->setDropAction(Qt::MoveAction); } event->setAccepted((event->mimeData()->hasText() && doc()->isReadWrite()) || event->mimeData()->hasUrls()); } void KateViewInternal::fixDropEvent(QDropEvent *event) { if (event->source() != this) { event->setDropAction(Qt::CopyAction); } else { Qt::DropAction action = Qt::MoveAction; #ifdef Q_WS_MAC if (event->keyboardModifiers() & Qt::AltModifier) { action = Qt::CopyAction; } #else if (event->keyboardModifiers() & Qt::ControlModifier) { action = Qt::CopyAction; } #endif event->setDropAction(action); } } void KateViewInternal::dragMoveEvent(QDragMoveEvent *event) { // track the cursor to the current drop location placeCursor(event->pos(), true, false); // important: accept action to switch between copy and move mode // without this, the text will always be copied. fixDropEvent(event); } void KateViewInternal::dropEvent(QDropEvent *event) { /** * if we have urls, pass this event off to the hosting application */ if (event->mimeData()->hasUrls()) { emit dropEventPass(event); return; } if (event->mimeData()->hasText() && doc()->isReadWrite()) { const QString text = event->mimeData()->text(); - - // is the source our own document? - bool priv = false; - if (KateViewInternal *vi = qobject_cast(event->source())) { - priv = doc()->ownedView(vi->m_view); - } - - // dropped on a text selection area? - bool selected = view()->cursorSelected(m_cursor); + const bool blockMode = view()->blockSelection(); fixDropEvent(event); - if (priv && selected && event->dropAction() != Qt::CopyAction) { - // this is a drag that we started and dropped on our selection - // ignore this case - return; - } + // Remember where to paste/move... + KTextEditor::Cursor targetCursor(m_cursor); + // Use powerful MovingCursor to track our changes we may do + std::unique_ptr targetCursor2(doc()->newMovingCursor(m_cursor)); - // fix the cursor position before editStart(), so that it is correctly - // stored for the undo action - KTextEditor::Cursor targetCursor(m_cursor); // backup current cursor - int selectionWidth = view()->selectionRange().columnWidth(); // for block selection - int selectionHeight = view()->selectionRange().numberOfLines(); // for block selection + // As always need the BlockMode some special treatment + const KTextEditor::Range selRange(view()->selectionRange()); + const KTextEditor::Cursor blockAdjust(selRange.numberOfLines(), selRange.columnWidth()); + // Restore the cursor position before editStart(), so that it is correctly stored for the undo action if (event->dropAction() != Qt::CopyAction) { - editSetCursor(view()->selectionRange().end()); + editSetCursor(selRange.end()); } else { view()->clearSelection(); } // use one transaction doc()->editStart(); - // on move: remove selected text; on copy: duplicate text - doc()->insertText(targetCursor, text, view()->blockSelection()); - - KTextEditor::DocumentCursor startCursor(doc(), targetCursor); - KTextEditor::DocumentCursor endCursor1(doc(), targetCursor); - const int textLength = text.length(); - if (event->dropAction() != Qt::CopyAction) { view()->removeSelectedText(); - if (m_cursor.toCursor() < startCursor.toCursor()) { - startCursor.move(-textLength); - endCursor1.move(-textLength); + if (targetCursor2->toCursor() != targetCursor) { + // Hm, multi line selection moved down, we need to adjust our dumb cursor + targetCursor = targetCursor2->toCursor(); } - } + doc()->insertText(targetCursor2->toCursor(), text, blockMode); - if (!view()->blockSelection()) { - endCursor1.move(textLength); } else { - endCursor1.setColumn(startCursor.column() + selectionWidth); - endCursor1.setLine(startCursor.line() + selectionHeight); + doc()->insertText(targetCursor, text, blockMode); } - KTextEditor::Cursor endCursor(endCursor1); - qCDebug(LOG_KTE) << startCursor << "---(" << textLength << ")---" << endCursor; - setSelection(KTextEditor::Range(startCursor, endCursor)); - editSetCursor(endCursor); + if (blockMode) { + setSelection(KTextEditor::Range(targetCursor, targetCursor + blockAdjust)); + editSetCursor(targetCursor + blockAdjust); + } else { + setSelection(KTextEditor::Range(targetCursor, targetCursor2->toCursor())); + editSetCursor(targetCursor2->toCursor()); // Just to satisfy autotest + } doc()->editEnd(); event->acceptProposedAction(); updateView(); } // finally finish drag and drop mode m_dragInfo.state = diNone; // important, because the eventFilter`s DragLeave does not occur stopDragScroll(); } //END EVENT HANDLING STUFF void KateViewInternal::clear() { m_startPos.setPosition(0, 0); m_displayCursor = KTextEditor::Cursor(0, 0); m_cursor.setPosition(0, 0); cache()->clear(); updateView(true); m_lineScroll->updatePixmap(); } void KateViewInternal::wheelEvent(QWheelEvent *e) { // check if this event should change the font size (Ctrl pressed, angle reported and not accidentally so) // Note: if detectZoomingEvent() doesn't unset the ControlModifier we'll get accelerated scrolling. if (m_zoomEventFilter->detectZoomingEvent(e)) { if (e->angleDelta().y() > 0) { slotIncFontSizes(qreal(e->angleDelta().y()) / QWheelEvent::DefaultDeltasPerStep); } else if (e->angleDelta().y() < 0) { slotDecFontSizes(qreal(-e->angleDelta().y()) / QWheelEvent::DefaultDeltasPerStep); } // accept always and be done for zooming e->accept(); return; } // handle vertical scrolling via the scrollbar if (e->orientation() == Qt::Vertical) { // compute distance auto sign = m_lineScroll->invertedControls() ? -1 : 1; auto offset = sign * qreal(e->angleDelta().y()) / 120.0; if ( e->modifiers() & Qt::ShiftModifier ) { const auto pageStep = m_lineScroll->pageStep(); offset = qBound(-pageStep, int(offset * pageStep), pageStep); } else { offset *= QApplication::wheelScrollLines(); } // handle accumulation m_accumulatedScroll += offset - int(offset); const auto extraAccumulated = int(m_accumulatedScroll); m_accumulatedScroll -= extraAccumulated; // do scroll scrollViewLines(int(offset) + extraAccumulated); e->accept(); } // handle horizontal scrolling via the scrollbar if (e->orientation() == Qt::Horizontal) { // if we have dyn word wrap, we should ignore the scroll events if (view()->dynWordWrap()) { e->accept(); return; } QWheelEvent copy = *e; QApplication::sendEvent(m_columnScroll, ©); if (copy.isAccepted()) { e->accept(); } } } void KateViewInternal::startDragScroll() { if (!m_dragScrollTimer.isActive()) { m_dragScrollTimer.start(s_scrollTime); } } void KateViewInternal::stopDragScroll() { m_dragScrollTimer.stop(); updateView(); } void KateViewInternal::doDragScroll() { QPoint p = this->mapFromGlobal(QCursor::pos()); int dx = 0, dy = 0; if (p.y() < s_scrollMargin) { dy = p.y() - s_scrollMargin; } else if (p.y() > height() - s_scrollMargin) { dy = s_scrollMargin - (height() - p.y()); } if (p.x() < s_scrollMargin) { dx = p.x() - s_scrollMargin; } else if (p.x() > width() - s_scrollMargin) { dx = s_scrollMargin - (width() - p.x()); } dy /= 4; if (dy) { scrollLines(startLine() + dy); } if (columnScrollingPossible() && dx) { scrollColumns(qMin(startX() + dx, m_columnScroll->maximum())); } if (!dy && !dx) { stopDragScroll(); } } void KateViewInternal::registerTextHintProvider(KTextEditor::TextHintProvider *provider) { if (! m_textHintProviders.contains(provider)) { m_textHintProviders.append(provider); } // we have a client, so start timeout m_textHintTimer.start(m_textHintDelay); } void KateViewInternal::unregisterTextHintProvider(KTextEditor::TextHintProvider *provider) { const int index = m_textHintProviders.indexOf(provider); if (index >= 0) { m_textHintProviders.removeAt(index); } if (m_textHintProviders.isEmpty()) { m_textHintTimer.stop(); } } void KateViewInternal::setTextHintDelay(int delay) { if (delay <= 0) { m_textHintDelay = 200; // ms } else { m_textHintDelay = delay; // ms } } int KateViewInternal::textHintDelay() const { return m_textHintDelay; } bool KateViewInternal::textHintsEnabled() { return ! m_textHintProviders.isEmpty(); } //BEGIN EDIT STUFF void KateViewInternal::editStart() { editSessionNumber++; if (editSessionNumber > 1) { return; } editIsRunning = true; editOldCursor = m_cursor; editOldSelection = view()->selectionRange(); } void KateViewInternal::editEnd(int editTagLineStart, int editTagLineEnd, bool tagFrom) { if (editSessionNumber == 0) { return; } editSessionNumber--; if (editSessionNumber > 0) { return; } // fix start position, might have moved from column 0 // try to clever calculate the right start column for the tricky dyn word wrap case int col = 0; if (view()->dynWordWrap()) { if (KateLineLayoutPtr layout = cache()->line(startLine())) { int index = layout->viewLineForColumn(startPos().column()); if (index >= 0 && index < layout->viewLineCount()) { col = layout->viewLine(index).startCol(); } } } m_startPos.setPosition(startLine(), col); if (tagFrom && (editTagLineStart <= int(view()->textFolding().visibleLineToLine(startLine())))) { tagAll(); } else { tagLines(editTagLineStart, tagFrom ? qMax(doc()->lastLine() + 1, editTagLineEnd) : editTagLineEnd, true); } if (editOldCursor == m_cursor.toCursor()) { updateBracketMarks(); } updateView(true); if (editOldCursor != m_cursor.toCursor() || m_view == doc()->activeView()) { // Only scroll the view to the cursor if the insertion happens at the cursor. // This might not be the case for e.g. collaborative editing, when a remote user // inserts text at a position not at the caret. if (m_cursor.line() >= editTagLineStart && m_cursor.line() <= editTagLineEnd) { m_madeVisible = false; updateCursor(m_cursor, true); } } /** * selection changed? * fixes bug 316226 */ if (editOldSelection != view()->selectionRange() || (editOldSelection.isValid() && !editOldSelection.isEmpty() && !(editTagLineStart > editOldSelection.end().line() && editTagLineEnd < editOldSelection.start().line()))) { emit view()->selectionChanged(m_view); } editIsRunning = false; } void KateViewInternal::editSetCursor(const KTextEditor::Cursor &_cursor) { if (m_cursor.toCursor() != _cursor) { m_cursor.setPosition(_cursor); } } //END void KateViewInternal::viewSelectionChanged() { if (!view()->selection()) { m_selectAnchor = KTextEditor::Cursor::invalid(); } else { m_selectAnchor = view()->selectionRange().start(); } // Do NOT nuke the entire range! The reason is that a shift+DC selection // might (correctly) set the range to be empty (i.e. start() == end()), and // subsequent dragging might shrink the selection into non-existence. When // this happens, we use the cached end to restore the cached start so that // updateSelection is not confused. See also comments in updateSelection. m_selectionCached.setStart(KTextEditor::Cursor::invalid()); } KateLayoutCache *KateViewInternal::cache() const { return m_layoutCache; } KTextEditor::Cursor KateViewInternal::toRealCursor(const KTextEditor::Cursor &virtualCursor) const { return KTextEditor::Cursor(view()->textFolding().visibleLineToLine(virtualCursor.line()), virtualCursor.column()); } KTextEditor::Cursor KateViewInternal::toVirtualCursor(const KTextEditor::Cursor &realCursor) const { /** * only convert valid lines, folding doesn't like invalid input! * don't validate whole cursor, column might be -1 */ if (realCursor.line() < 0) { return KTextEditor::Cursor::invalid(); } return KTextEditor::Cursor(view()->textFolding().lineToVisibleLine(realCursor.line()), realCursor.column()); } KateRenderer *KateViewInternal::renderer() const { return view()->renderer(); } void KateViewInternal::mouseMoved() { view()->notifyMousePositionChanged(m_mouse); view()->updateRangesIn(KTextEditor::Attribute::ActivateMouseIn); } void KateViewInternal::cursorMoved() { view()->updateRangesIn(KTextEditor::Attribute::ActivateCaretIn); #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive()) { QAccessibleTextCursorEvent ev(this, static_cast(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, m_cursor)); QAccessible::updateAccessibility(&ev); } #endif } bool KateViewInternal::rangeAffectsView(const KTextEditor::Range &range, bool realCursors) const { int startLine = KateViewInternal::startLine(); int endLine = startLine + (int)m_visibleLineCount; if (realCursors) { startLine = (int)view()->textFolding().visibleLineToLine(startLine); endLine = (int)view()->textFolding().visibleLineToLine(endLine); } return (range.end().line() >= startLine) || (range.start().line() <= endLine); } //BEGIN IM INPUT STUFF QVariant KateViewInternal::inputMethodQuery(Qt::InputMethodQuery query) const { switch (query) { case Qt::ImCursorRectangle: { // Cursor placement code is changed for Asian input method that // shows candidate window. This behavior is same as Qt/E 2.3.7 // which supports Asian input methods. Asian input methods need // start point of IM selection text to place candidate window as // adjacent to the selection text. // // in Qt5, cursor rectangle is used as QRectF internally, and it // will be checked by QRectF::isValid(), which will mark rectangle // with width == 0 or height == 0 as invalid. auto lineHeight = renderer()->lineHeight(); return QRect(cursorToCoordinate(m_cursor, true, false), QSize(1, lineHeight ? lineHeight : 1)); } case Qt::ImFont: return renderer()->currentFont(); case Qt::ImCursorPosition: return m_cursor.column(); case Qt::ImAnchorPosition: // If selectAnchor is at the same line, return the real anchor position // Otherwise return the same position of cursor if (view()->selection() && m_selectAnchor.line() == m_cursor.line()) { return m_selectAnchor.column(); } else { return m_cursor.column(); } case Qt::ImSurroundingText: if (Kate::TextLine l = doc()->kateTextLine(m_cursor.line())) { return l->string(); } else { return QString(); } case Qt::ImCurrentSelection: if (view()->selection()) { return view()->selectionText(); } else { return QString(); } default: /* values: ImMaximumTextLength */ break; } return QWidget::inputMethodQuery(query); } void KateViewInternal::inputMethodEvent(QInputMethodEvent *e) { if (doc()->readOnly()) { e->ignore(); return; } //qCDebug(LOG_KTE) << "Event: cursor" << m_cursor << "commit" << e->commitString() << "preedit" << e->preeditString() << "replacement start" << e->replacementStart() << "length" << e->replacementLength(); if (!m_imPreeditRange) { m_imPreeditRange = doc()->newMovingRange(KTextEditor::Range(m_cursor, m_cursor), KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); } if (!m_imPreeditRange->toRange().isEmpty()) { doc()->inputMethodStart(); doc()->removeText(*m_imPreeditRange); doc()->inputMethodEnd(); } if (!e->commitString().isEmpty() || e->replacementLength()) { view()->removeSelectedText(); KTextEditor::Range preeditRange = *m_imPreeditRange; KTextEditor::Cursor start(m_imPreeditRange->start().line(), m_imPreeditRange->start().column() + e->replacementStart()); KTextEditor::Cursor removeEnd = start + KTextEditor::Cursor(0, e->replacementLength()); doc()->editStart(); if (start != removeEnd) { doc()->removeText(KTextEditor::Range(start, removeEnd)); } if (!e->commitString().isEmpty()) { // if the input method event is text that should be inserted, call KTextEditor::DocumentPrivate::typeChars() // with the text. that method will handle the input and take care of overwrite mode, etc. doc()->typeChars(m_view, e->commitString()); } doc()->editEnd(); // Revert to the same range as above m_imPreeditRange->setRange(preeditRange); } if (!e->preeditString().isEmpty()) { doc()->inputMethodStart(); doc()->insertText(m_imPreeditRange->start(), e->preeditString()); doc()->inputMethodEnd(); // The preedit range gets automatically repositioned } // Finished this input method context? if (m_imPreeditRange && e->preeditString().isEmpty()) { // delete the range and reset the pointer delete m_imPreeditRange; m_imPreeditRange = nullptr; qDeleteAll(m_imPreeditRangeChildren); m_imPreeditRangeChildren.clear(); if (QApplication::cursorFlashTime() > 0) { renderer()->setDrawCaret(false); } renderer()->setCaretOverrideColor(QColor()); e->accept(); return; } KTextEditor::Cursor newCursor = m_cursor; bool hideCursor = false; QColor caretColor; if (m_imPreeditRange) { qDeleteAll(m_imPreeditRangeChildren); m_imPreeditRangeChildren.clear(); int decorationColumn = 0; foreach (const QInputMethodEvent::Attribute &a, e->attributes()) { if (a.type == QInputMethodEvent::Cursor) { newCursor = m_imPreeditRange->start() + KTextEditor::Cursor(0, a.start); hideCursor = !a.length; QColor c = qvariant_cast(a.value); if (c.isValid()) { caretColor = c; } } else if (a.type == QInputMethodEvent::TextFormat) { QTextCharFormat f = qvariant_cast(a.value).toCharFormat(); if (f.isValid() && decorationColumn <= a.start) { KTextEditor::Range fr(m_imPreeditRange->start().line(), m_imPreeditRange->start().column() + a.start, m_imPreeditRange->start().line(), m_imPreeditRange->start().column() + a.start + a.length); KTextEditor::MovingRange *formatRange = doc()->newMovingRange(fr); KTextEditor::Attribute::Ptr attribute(new KTextEditor::Attribute()); attribute->merge(f); formatRange->setAttribute(attribute); decorationColumn = a.start + a.length; m_imPreeditRangeChildren.push_back(formatRange); } } } } renderer()->setDrawCaret(hideCursor); renderer()->setCaretOverrideColor(caretColor); if (newCursor != m_cursor.toCursor()) { updateCursor(newCursor); } e->accept(); } //END IM INPUT STUFF void KateViewInternal::flashChar(const KTextEditor::Cursor &pos, KTextEditor::Attribute::Ptr attribute) { Q_ASSERT(pos.isValid()); Q_ASSERT(attribute.constData()); // if line is folded away, do nothing if (!view()->textFolding().isLineVisible(pos.line())) { return; } KTextEditor::Range range(pos, KTextEditor::Cursor(pos.line(), pos.column() + 1)); if (m_textAnimation) { m_textAnimation->deleteLater(); } m_textAnimation = new KateTextAnimation(range, attribute, this); } void KateViewInternal::documentTextInserted(KTextEditor::Document *document, const KTextEditor::Range &range) { #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive()) { QAccessibleTextInsertEvent ev(this, static_cast(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, range.start()), document->text(range)); QAccessible::updateAccessibility(&ev); } #endif } void KateViewInternal::documentTextRemoved(KTextEditor::Document * /*document*/, const KTextEditor::Range &range, const QString &oldText) { #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive()) { QAccessibleTextRemoveEvent ev(this, static_cast(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, range.start()), oldText); QAccessible::updateAccessibility(&ev); } #endif } QRect KateViewInternal::inlineNoteRect(const KateInlineNoteData& noteData) const { KTextEditor::InlineNote note(noteData); // compute note width and position const auto noteWidth = note.width(); auto noteCursor = note.position(); // The cursor might be outside of the text. In that case, clamp it to the text and // later on add the missing x offset. const auto lineLength = view()->document()->lineLength(noteCursor.line()); int extraOffset = -noteWidth; if (noteCursor.column() == lineLength) { extraOffset = 0; } else if (noteCursor.column() > lineLength) { extraOffset = (noteCursor.column() - lineLength) * renderer()->spaceWidth(); noteCursor.setColumn(lineLength); } auto noteStartPos = mapToGlobal(cursorToCoordinate(noteCursor, true, false)); // compute the note's rect auto globalNoteRect = QRect(noteStartPos + QPoint{extraOffset, 0}, QSize(noteWidth, renderer()->lineHeight())); return globalNoteRect; } KateInlineNoteData KateViewInternal::inlineNoteAt(const QPoint& globalPos) const { // compute the associated cursor to get the right line const int line = coordinatesToCursor(mapFromGlobal(globalPos)).line(); const auto inlineNotes = view()->inlineNotes(line); // loop over all notes and check if the point is inside it foreach (const auto& note, inlineNotes) { auto globalNoteRect = inlineNoteRect(note); if (globalNoteRect.contains(globalPos)) { return note; } } // none found -- return an invalid note return {}; }