diff --git a/src/render/katerenderer.cpp b/src/render/katerenderer.cpp index 5896284e..90772357 100644 --- a/src/render/katerenderer.cpp +++ b/src/render/katerenderer.cpp @@ -1,1213 +1,1225 @@ /* This file is part of the KDE libraries Copyright (C) 2007 Mirko Stocker Copyright (C) 2003-2005 Hamish Rodda Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2013 Andrey Matveyakin 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 "katerenderer.h" #include "katedocument.h" #include "kateconfig.h" #include "katehighlight.h" #include "kateview.h" #include "katerenderrange.h" #include "katetextlayout.h" #include "katebuffer.h" #include "inlinenotedata.h" #include "ktexteditor/inlinenote.h" #include "ktexteditor/inlinenoteprovider.h" #include "katepartdebug.h" #include #include #include #include #include #include #include // qCeil static const QChar tabChar(QLatin1Char('\t')); static const QChar spaceChar(QLatin1Char(' ')); static const QChar nbSpaceChar(0xa0); // non-breaking space KateRenderer::KateRenderer(KTextEditor::DocumentPrivate *doc, Kate::TextFolding &folding, KTextEditor::ViewPrivate *view) : m_doc(doc) , m_folding(folding) , m_view(view) , m_tabWidth(m_doc->config()->tabWidth()) , m_indentWidth(m_doc->config()->indentationWidth()) , m_caretStyle(KateRenderer::Line) , m_drawCaret(true) , m_showSelections(true) , m_showTabs(true) , m_showSpaces(KateDocumentConfig::Trailing) , m_showNonPrintableSpaces(false) , m_printerFriendly(false) , m_config(new KateRendererConfig(this)) { updateAttributes(); // initialize with a sane font height updateFontHeight(); // make the proper calculation for markerSize updateMarkerSize(); } KateRenderer::~KateRenderer() { delete m_config; } void KateRenderer::updateAttributes() { m_attributes = m_doc->highlight()->attributes(config()->schema()); } KTextEditor::Attribute::Ptr KateRenderer::attribute(uint pos) const { if (pos < (uint)m_attributes.count()) { return m_attributes[pos]; } return m_attributes[0]; } KTextEditor::Attribute::Ptr KateRenderer::specificAttribute(int context) const { if (context >= 0 && context < m_attributes.count()) { return m_attributes[context]; } return m_attributes[0]; } void KateRenderer::setDrawCaret(bool drawCaret) { m_drawCaret = drawCaret; } void KateRenderer::setCaretStyle(KateRenderer::caretStyles style) { m_caretStyle = style; } void KateRenderer::setShowTabs(bool showTabs) { m_showTabs = showTabs; } void KateRenderer::setShowSpaces(KateDocumentConfig::WhitespaceRendering showSpaces) { m_showSpaces = showSpaces; } void KateRenderer::setShowNonPrintableSpaces(const bool on) { m_showNonPrintableSpaces = on; } void KateRenderer::setTabWidth(int tabWidth) { m_tabWidth = tabWidth; } bool KateRenderer::showIndentLines() const { return m_config->showIndentationLines(); } void KateRenderer::setShowIndentLines(bool showIndentLines) { m_config->setShowIndentationLines(showIndentLines); } void KateRenderer::setIndentWidth(int indentWidth) { m_indentWidth = indentWidth; } void KateRenderer::setShowSelections(bool showSelections) { m_showSelections = showSelections; } void KateRenderer::increaseFontSizes(qreal step) { QFont f(config()->font()); f.setPointSizeF(f.pointSizeF() + step); config()->setFont(f); } void KateRenderer::decreaseFontSizes(qreal step) { QFont f(config()->font()); if ((f.pointSizeF() - step) > 0) { f.setPointSizeF(f.pointSizeF() - step); } config()->setFont(f); } bool KateRenderer::isPrinterFriendly() const { return m_printerFriendly; } void KateRenderer::setPrinterFriendly(bool printerFriendly) { m_printerFriendly = printerFriendly; setShowTabs(false); setShowSpaces(KateDocumentConfig::None); setShowSelections(false); setDrawCaret(false); } void KateRenderer::paintTextLineBackground(QPainter &paint, KateLineLayoutPtr layout, int currentViewLine, int xStart, int xEnd) { if (isPrinterFriendly()) { return; } // Normal background color QColor backgroundColor(config()->backgroundColor()); // paint the current line background if we're on the current line QColor currentLineColor = config()->highlightedLineColor(); // Check for mark background int markRed = 0, markGreen = 0, markBlue = 0, markCount = 0; // Retrieve marks for this line uint mrk = m_doc->mark(layout->line()); if (mrk) { for (uint bit = 0; bit < 32; bit++) { KTextEditor::MarkInterface::MarkTypes markType = (KTextEditor::MarkInterface::MarkTypes)(1 << bit); if (mrk & markType) { QColor markColor = config()->lineMarkerColor(markType); if (markColor.isValid()) { markCount++; markRed += markColor.red(); markGreen += markColor.green(); markBlue += markColor.blue(); } } } // for } // Marks if (markCount) { markRed /= markCount; markGreen /= markCount; markBlue /= markCount; backgroundColor.setRgb( int((backgroundColor.red() * 0.9) + (markRed * 0.1)), int((backgroundColor.green() * 0.9) + (markGreen * 0.1)), int((backgroundColor.blue() * 0.9) + (markBlue * 0.1)) ); } // Draw line background paint.fillRect(0, 0, xEnd - xStart, lineHeight() * layout->viewLineCount(), backgroundColor); // paint the current line background if we're on the current line if (currentViewLine != -1) { if (markCount) { markRed /= markCount; markGreen /= markCount; markBlue /= markCount; currentLineColor.setRgb( int((currentLineColor.red() * 0.9) + (markRed * 0.1)), int((currentLineColor.green() * 0.9) + (markGreen * 0.1)), int((currentLineColor.blue() * 0.9) + (markBlue * 0.1)) ); } paint.fillRect(0, lineHeight() * currentViewLine, xEnd - xStart, lineHeight(), currentLineColor); } } void KateRenderer::paintTabstop(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); paint.setPen(pen); paint.setRenderHint(QPainter::Antialiasing, false); int dist = spaceWidth() * 0.3; QPoint points[8]; points[0] = QPoint(x - dist, y - dist); points[1] = QPoint(x, y); points[2] = QPoint(x, y); points[3] = QPoint(x - dist, y + dist); x += spaceWidth() / 3.0; points[4] = QPoint(x - dist, y - dist); points[5] = QPoint(x, y); points[6] = QPoint(x, y); points[7] = QPoint(x - dist, y + dist); paint.drawLines(points, 4); paint.setPen(penBackup); } void KateRenderer::paintSpace(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(m_markerSize); pen.setCapStyle(Qt::RoundCap); paint.setPen(pen); paint.setRenderHint(QPainter::Antialiasing, true); paint.drawPoint(QPointF(x, y)); paint.setPen(penBackup); } void KateRenderer::paintNonBreakSpace(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); paint.setPen(pen); paint.setRenderHint(QPainter::Antialiasing, false); const int height = fontHeight(); const int width = spaceWidth(); QPoint points[6]; points[0] = QPoint(x + width / 10, y + height / 4); points[1] = QPoint(x + width / 10, y + height / 3); points[2] = QPoint(x + width / 10, y + height / 3); points[3] = QPoint(x + width - width / 10, y + height / 3); points[4] = QPoint(x + width - width / 10, y + height / 3); points[5] = QPoint(x + width - width / 10, y + height / 4); paint.drawLines(points, 3); paint.setPen(penBackup); } void KateRenderer::paintNonPrintableSpaces(QPainter &paint, qreal x, qreal y, const QChar &chr) { paint.save(); QPen pen(config()->spellingMistakeLineColor()); pen.setWidthF(qMax(1.0, spaceWidth() * 0.1)); paint.setPen(pen); paint.setRenderHint(QPainter::Antialiasing, false); const int height = fontHeight(); const int width = config()->fontMetrics().width(chr); const int offset = spaceWidth() * 0.1; QPoint points[8]; points[0] = QPoint(x - offset, y + offset); points[1] = QPoint(x + width + offset, y + offset); points[2] = QPoint(x + width + offset, y + offset); points[3] = QPoint(x + width + offset, y - height - offset); points[4] = QPoint(x + width + offset, y - height - offset); points[5] = QPoint(x - offset, y - height - offset); points[6] = QPoint(x - offset, y - height - offset); points[7] = QPoint(x - offset, y + offset); paint.drawLines(points, 4); paint.restore(); } void KateRenderer::paintIndentMarker(QPainter &paint, uint x, uint y /*row*/) { QPen penBackup(paint.pen()); QPen myPen(config()->indentationLineColor()); static const QVector dashPattern = QVector() << 1 << 1; myPen.setDashPattern(dashPattern); if (y % 2) { myPen.setDashOffset(1); } paint.setPen(myPen); const int height = fontHeight(); const int top = 0; const int bottom = height - 1; QPainter::RenderHints renderHints = paint.renderHints(); paint.setRenderHints(renderHints, false); paint.drawLine(x + 2, top, x + 2, bottom); paint.setRenderHints(renderHints, true); paint.setPen(penBackup); } static bool rangeLessThanForRenderer(const Kate::TextRange *a, const Kate::TextRange *b) { // compare Z-Depth first // smaller Z-Depths should win! if (a->zDepth() > b->zDepth()) { return true; } else if (a->zDepth() < b->zDepth()) { return false; } // end of a > end of b? if (a->end().toCursor() > b->end().toCursor()) { return true; } // if ends are equal, start of a < start of b? if (a->end().toCursor() == b->end().toCursor()) { return a->start().toCursor() < b->start().toCursor(); } return false; } QVector KateRenderer::decorationsForLine(const Kate::TextLine &textLine, int line, bool selectionsOnly, KateRenderRange *completionHighlight, bool completionSelected) const { QVector newHighlight; // Don't compute the highlighting if there isn't going to be any highlighting QList rangesWithAttributes = m_doc->buffer().rangesForLine(line, m_printerFriendly ? nullptr : m_view, true); if (selectionsOnly || !textLine->attributesList().isEmpty() || !rangesWithAttributes.isEmpty()) { RenderRangeList renderRanges; // Add the inbuilt highlighting to the list NormalRenderRange *inbuiltHighlight = new NormalRenderRange(); const QVector &al = textLine->attributesList(); for (int i = 0; i < al.count(); ++i) if (al[i].length > 0 && al[i].attributeValue > 0) { inbuiltHighlight->addRange(new KTextEditor::Range(KTextEditor::Cursor(line, al[i].offset), al[i].length), specificAttribute(al[i].attributeValue)); } renderRanges.append(inbuiltHighlight); if (!completionHighlight) { // check for dynamic hl stuff const QSet *rangesMouseIn = m_view ? m_view->rangesMouseIn() : nullptr; const QSet *rangesCaretIn = m_view ? m_view->rangesCaretIn() : nullptr; bool anyDynamicHlsActive = m_view && (!rangesMouseIn->empty() || !rangesCaretIn->empty()); // sort all ranges, we want that the most specific ranges win during rendering, multiple equal ranges are kind of random, still better than old smart rangs behavior ;) qSort(rangesWithAttributes.begin(), rangesWithAttributes.end(), rangeLessThanForRenderer); // loop over all ranges for (int i = 0; i < rangesWithAttributes.size(); ++i) { // real range Kate::TextRange *kateRange = rangesWithAttributes[i]; // calculate attribute, default: normal attribute KTextEditor::Attribute::Ptr attribute = kateRange->attribute(); if (anyDynamicHlsActive) { // check mouse in if (KTextEditor::Attribute::Ptr attributeMouseIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)) { if (rangesMouseIn->contains(kateRange)) { attribute = attributeMouseIn; } } // check caret in if (KTextEditor::Attribute::Ptr attributeCaretIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateCaretIn)) { if (rangesCaretIn->contains(kateRange)) { attribute = attributeCaretIn; } } } // span range NormalRenderRange *additionaHl = new NormalRenderRange(); additionaHl->addRange(new KTextEditor::Range(*kateRange), attribute); renderRanges.append(additionaHl); } } else { // Add the code completion arbitrary highlight to the list renderRanges.append(completionHighlight); } // Add selection highlighting if we're creating the selection decorations if ((m_view && selectionsOnly && showSelections() && m_view->selection()) || (completionHighlight && completionSelected) || (m_view && m_view->blockSelection())) { NormalRenderRange *selectionHighlight = new NormalRenderRange(); // Set up the selection background attribute TODO: move this elsewhere, eg. into the config? static KTextEditor::Attribute::Ptr backgroundAttribute; if (!backgroundAttribute) { backgroundAttribute = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); } backgroundAttribute->setBackground(config()->selectionColor()); backgroundAttribute->setForeground(attribute(KTextEditor::dsNormal)->selectedForeground().color()); // Create a range for the current selection if (completionHighlight && completionSelected) { selectionHighlight->addRange(new KTextEditor::Range(line, 0, line + 1, 0), backgroundAttribute); } else if (m_view->blockSelection() && m_view->selectionRange().overlapsLine(line)) { selectionHighlight->addRange(new KTextEditor::Range(m_doc->rangeOnLine(m_view->selectionRange(), line)), backgroundAttribute); } else { selectionHighlight->addRange(new KTextEditor::Range(m_view->selectionRange()), backgroundAttribute); } renderRanges.append(selectionHighlight); // highlighting for the vi visual modes } KTextEditor::Cursor currentPosition, endPosition; // Calculate the range which we need to iterate in order to get the highlighting for just this line if (m_view && selectionsOnly) { if (m_view->blockSelection()) { KTextEditor::Range subRange = m_doc->rangeOnLine(m_view->selectionRange(), line); currentPosition = subRange.start(); endPosition = subRange.end(); } else { KTextEditor::Range rangeNeeded = m_view->selectionRange() & KTextEditor::Range(line, 0, line + 1, 0); currentPosition = qMax(KTextEditor::Cursor(line, 0), rangeNeeded.start()); endPosition = qMin(KTextEditor::Cursor(line + 1, 0), rangeNeeded.end()); } } else { currentPosition = KTextEditor::Cursor(line, 0); endPosition = KTextEditor::Cursor(line + 1, 0); } // Main iterative loop. This walks through each set of highlighting ranges, and stops each // time the highlighting changes. It then creates the corresponding QTextLayout::FormatRanges. while (currentPosition < endPosition) { renderRanges.advanceTo(currentPosition); if (!renderRanges.hasAttribute()) { // No attribute, don't need to create a FormatRange for this text range currentPosition = renderRanges.nextBoundary(); continue; } KTextEditor::Cursor nextPosition = renderRanges.nextBoundary(); // Create the format range and populate with the correct start, length and format info QTextLayout::FormatRange fr; fr.start = currentPosition.column(); if (nextPosition < endPosition || endPosition.line() <= line) { fr.length = nextPosition.column() - currentPosition.column(); } else { // +1 to force background drawing at the end of the line when it's warranted fr.length = textLine->length() - currentPosition.column() + 1; } KTextEditor::Attribute::Ptr a = renderRanges.generateAttribute(); if (a) { fr.format = *a; if (selectionsOnly) { assignSelectionBrushesFromAttribute(fr, *a); } } newHighlight.append(fr); currentPosition = nextPosition; } if (completionHighlight) // Don't delete external completion render range { renderRanges.removeAll(completionHighlight); } qDeleteAll(renderRanges); } return newHighlight; } void KateRenderer::assignSelectionBrushesFromAttribute(QTextLayout::FormatRange &target, const KTextEditor::Attribute &attribute) const { if (attribute.hasProperty(SelectedForeground)) { target.format.setForeground(attribute.selectedForeground()); } if (attribute.hasProperty(SelectedBackground)) { target.format.setBackground(attribute.selectedBackground()); } } void KateRenderer::paintTextLine(QPainter &paint, KateLineLayoutPtr range, int xStart, int xEnd, const KTextEditor::Cursor *cursor, PaintTextLineFlags flags) { Q_ASSERT(range->isValid()); // qCDebug(LOG_KTE)<<"KateRenderer::paintTextLine"; // font data const QFontMetricsF &fm = config()->fontMetrics(); int currentViewLine = -1; if (cursor && cursor->line() == range->line()) { currentViewLine = range->viewLineForColumn(cursor->column()); } paintTextLineBackground(paint, range, currentViewLine, xStart, xEnd); if (range->layout()) { bool drawSelection = m_view && m_view->selection() && showSelections() && m_view->selectionRange().overlapsLine(range->line()); // Draw selection in block selection mode. We need 2 kinds of selections that QTextLayout::draw can't render: // - past-end-of-line selection and // - 0-column-wide selection (used to indicate where text will be typed) if (drawSelection && m_view->blockSelection()) { int selectionStartColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().start())); int selectionEndColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().end())); QBrush selectionBrush = config()->selectionColor(); if (selectionStartColumn != selectionEndColumn) { KateTextLayout lastLine = range->viewLine(range->viewLineCount() - 1); if (selectionEndColumn > lastLine.startCol()) { int selectionStartX = (selectionStartColumn > lastLine.startCol()) ? cursorToX(lastLine, selectionStartColumn, true) : 0; int selectionEndX = cursorToX(lastLine, selectionEndColumn, true); paint.fillRect(QRect(selectionStartX - xStart, (int)lastLine.lineLayout().y(), selectionEndX - selectionStartX, lineHeight()), selectionBrush); } } else { const int selectStickWidth = 2; KateTextLayout selectionLine = range->viewLine(range->viewLineForColumn(selectionStartColumn)); int selectionX = cursorToX(selectionLine, selectionStartColumn, true); paint.fillRect(QRect(selectionX - xStart, (int)selectionLine.lineLayout().y(), selectStickWidth, lineHeight()), selectionBrush); } } QVector additionalFormats; if (range->length() > 0) { // We may have changed the pen, be absolutely sure it gets set back to // normal foreground color before drawing text for text that does not // set the pen color paint.setPen(attribute(KTextEditor::dsNormal)->foreground().color()); // Draw the text :) if (drawSelection) { additionalFormats = decorationsForLine(range->textLine(), range->line(), true); range->layout()->draw(&paint, QPoint(-xStart, 0), additionalFormats); } else { range->layout()->draw(&paint, QPoint(-xStart, 0)); } } QBrush backgroundBrush; bool backgroundBrushSet = false; // Loop each individual line for additional text decoration etc. QListIterator it = range->layout()->additionalFormats(); QVectorIterator it2 = additionalFormats; for (int i = 0; i < range->viewLineCount(); ++i) { KateTextLayout line = range->viewLine(i); bool haveBackground = false; // Determine the background to use, if any, for the end of this view line backgroundBrushSet = false; while (it2.hasNext()) { const QTextLayout::FormatRange &fr = it2.peekNext(); if (fr.start > line.endCol()) { break; } if (fr.start + fr.length > line.endCol()) { if (fr.format.hasProperty(QTextFormat::BackgroundBrush)) { backgroundBrushSet = true; backgroundBrush = fr.format.background(); } haveBackground = true; break; } it2.next(); } while (!haveBackground && it.hasNext()) { const QTextLayout::FormatRange &fr = it.peekNext(); if (fr.start > line.endCol()) { break; } if (fr.start + fr.length > line.endCol()) { if (fr.format.hasProperty(QTextFormat::BackgroundBrush)) { backgroundBrushSet = true; backgroundBrush = fr.format.background(); } break; } it.next(); } // Draw selection or background color outside of areas where text is rendered if (!m_printerFriendly) { bool draw = false; QBrush drawBrush; if (m_view && m_view->selection() && !m_view->blockSelection() && m_view->lineEndSelected(line.end(true))) { draw = true; drawBrush = config()->selectionColor(); } else if (backgroundBrushSet && !m_view->blockSelection()) { draw = true; drawBrush = backgroundBrush; } if (draw) { int fillStartX = line.endX() - line.startX() + line.xOffset() - xStart; int fillStartY = lineHeight() * i; int width = xEnd - xStart - fillStartX; int height = lineHeight(); // reverse X for right-aligned lines if (range->layout()->textOption().alignment() == Qt::AlignRight) { fillStartX = 0; } if (width > 0) { QRect area(fillStartX, fillStartY, width, height); paint.fillRect(area, drawBrush); } } // Draw indent lines if (showIndentLines() && i == 0) { const qreal w = spaceWidth(); const int lastIndentColumn = range->textLine()->indentDepth(m_tabWidth); for (int x = m_indentWidth; x < lastIndentColumn; x += m_indentWidth) { paintIndentMarker(paint, x * w + 1 - xStart, range->line()); } } } // draw an open box to mark non-breaking spaces const QString &text = range->textLine()->string(); int y = lineHeight() * i + fm.ascent() - fm.strikeOutPos(); int nbSpaceIndex = text.indexOf(nbSpaceChar, line.lineLayout().xToCursor(xStart)); while (nbSpaceIndex != -1 && nbSpaceIndex < line.endCol()) { int x = line.lineLayout().cursorToX(nbSpaceIndex); if (x > xEnd) { break; } paintNonBreakSpace(paint, x - xStart, y); nbSpaceIndex = text.indexOf(nbSpaceChar, nbSpaceIndex + 1); } // draw tab stop indicators if (showTabs()) { int tabIndex = text.indexOf(tabChar, line.lineLayout().xToCursor(xStart)); while (tabIndex != -1 && tabIndex < line.endCol()) { int x = line.lineLayout().cursorToX(tabIndex); if (x > xEnd) { break; } paintTabstop(paint, x - xStart + spaceWidth() / 2.0, y); tabIndex = text.indexOf(tabChar, tabIndex + 1); } } // draw trailing spaces if (showSpaces() != KateDocumentConfig::None) { int spaceIndex = line.endCol() - 1; const int trailingPos = showSpaces() == KateDocumentConfig::All ? 0 : qMax(range->textLine()->lastChar(), 0); if (spaceIndex >= trailingPos) { for (; spaceIndex >= line.startCol(); --spaceIndex) { if (!text.at(spaceIndex).isSpace()) { if (showSpaces() == KateDocumentConfig::Trailing) break; else continue; } if (text.at(spaceIndex) != QLatin1Char('\t') || !showTabs()) { if (range->layout()->textOption().alignment() == Qt::AlignRight) { // Draw on left for RTL lines paintSpace(paint, line.lineLayout().cursorToX(spaceIndex) - xStart - spaceWidth() / 2.0, y); } else { paintSpace(paint, line.lineLayout().cursorToX(spaceIndex) - xStart + spaceWidth() / 2.0, y); } } } } } if (showNonPrintableSpaces()) { const int y = lineHeight() * i + fm.ascent(); static const QRegularExpression nonPrintableSpacesRegExp(QStringLiteral("[\\x{2000}-\\x{200F}\\x{2028}-\\x{202F}\\x{205F}-\\x{2064}\\x{206A}-\\x{206F}]")); QRegularExpressionMatchIterator i = nonPrintableSpacesRegExp.globalMatch(text, line.lineLayout().xToCursor(xStart)); while (i.hasNext()) { const int charIndex = i.next().capturedStart(); const int x = line.lineLayout().cursorToX(charIndex); if (x > xEnd) { break; } paintNonPrintableSpaces(paint, x - xStart, y, text[charIndex]); } } } // Draw inline notes if (!isPrinterFriendly()) { const auto inlineNotes = m_view->inlineNotes(range->line()); for (const auto& inlineNoteData: inlineNotes) { KTextEditor::InlineNote inlineNote(inlineNoteData); const int column = inlineNote.position().column(); int viewLine = range->viewLineForColumn(column); // Determine the position where to paint the note. // We start by getting the x coordinate of cursor placed to the column. qreal x = range->viewLine(viewLine).lineLayout().cursorToX(column) - xStart; int textLength = range->length(); if (column == 0 || column < textLength) { // If the note is inside text or at the beginning, then there is a hole in the text where the // note should be painted and the cursor gets placed at the right side of it. So we have to // subtract the width of the note to get to left side of the hole. x -= inlineNote.width(); } else { // If the note is outside the text, then the X coordinate is located at the end of the line. // Add appropriate amount of spaces to reach the required column. x += spaceWidth() * (column - textLength); } qreal y = lineHeight() * viewLine; // Paint the note paint.save(); paint.translate(x, y); inlineNote.provider()->paintInlineNote(inlineNote, paint); paint.restore(); } } // draw word-wrap-honor-indent filling if ((range->viewLineCount() > 1) && range->shiftX() && (range->shiftX() > xStart)) { if (backgroundBrushSet) paint.fillRect(0, lineHeight(), range->shiftX() - xStart, lineHeight() * (range->viewLineCount() - 1), backgroundBrush); paint.fillRect(0, lineHeight(), range->shiftX() - xStart, lineHeight() * (range->viewLineCount() - 1), QBrush(config()->wordWrapMarkerColor(), Qt::Dense4Pattern)); } // Draw caret if (drawCaret() && cursor && range->includesCursor(*cursor)) { int caretWidth, lineWidth = 2; QColor color; QTextLine line = range->layout()->lineForTextPosition(qMin(cursor->column(), range->length())); // Determine the caret's style caretStyles style = caretStyle(); // Make the caret the desired width if (style == Line) { caretWidth = lineWidth; } else if (line.isValid() && cursor->column() < range->length()) { caretWidth = int(line.cursorToX(cursor->column() + 1) - line.cursorToX(cursor->column())); if (caretWidth < 0) { caretWidth = -caretWidth; } } else { caretWidth = spaceWidth(); } // Determine the color if (m_caretOverrideColor.isValid()) { // Could actually use the real highlighting system for this... // would be slower, but more accurate for corner cases color = m_caretOverrideColor; } else { // search for the FormatRange that includes the cursor foreach (const QTextLayout::FormatRange &r, range->layout()->additionalFormats()) { if ((r.start <= cursor->column()) && ((r.start + r.length) > cursor->column())) { // check for Qt::NoBrush, as the returned color is black() and no invalid QColor QBrush foregroundBrush = r.format.foreground(); if (foregroundBrush != Qt::NoBrush) { color = r.format.foreground().color(); } break; } } // still no color found, fall back to default style if (!color.isValid()) { color = attribute(KTextEditor::dsNormal)->foreground().color(); } } paint.save(); paint.setRenderHint(QPainter::Antialiasing, false); switch (style) { case Line : paint.setPen(QPen(color, caretWidth)); break; case Block : // use a gray caret so it's possible to see the character color.setAlpha(128); paint.setPen(QPen(color, caretWidth)); break; case Underline : break; case Half : color.setAlpha(128); paint.setPen(QPen(color, caretWidth)); break; } if (cursor->column() <= range->length()) { range->layout()->drawCursor(&paint, QPoint(-xStart, 0), cursor->column(), caretWidth); } else { // Off the end of the line... must be block mode. Draw the caret ourselves. const KateTextLayout &lastLine = range->viewLine(range->viewLineCount() - 1); int x = cursorToX(lastLine, KTextEditor::Cursor(range->line(), cursor->column()), true); if ((x >= xStart) && (x <= xEnd)) { paint.fillRect(x - xStart, (int)lastLine.lineLayout().y(), caretWidth, lineHeight(), color); } } paint.restore(); } } // Draws the dashed underline at the start of a folded block of text. if (!(flags & SkipDrawFirstInvisibleLineUnderlined) && range->startsInvisibleBlock()) { const QPainter::RenderHints backupRenderHints = paint.renderHints(); paint.setRenderHint(QPainter::Antialiasing, false); QPen pen(config()->wordWrapMarkerColor()); pen.setCosmetic(true); pen.setStyle(Qt::DashLine); pen.setDashOffset(xStart); paint.setPen(pen); paint.drawLine(0, (lineHeight() * range->viewLineCount()) - 1, xEnd - xStart, (lineHeight() * range->viewLineCount()) - 1); paint.setRenderHints(backupRenderHints); } // show word wrap marker if desirable if ((!isPrinterFriendly()) && config()->wordWrapMarker() && QFontInfo(config()->font()).fixedPitch()) { const QPainter::RenderHints backupRenderHints = paint.renderHints(); paint.setRenderHint(QPainter::Antialiasing, false); paint.setPen(config()->wordWrapMarkerColor()); int _x = qreal(m_doc->config()->wordWrapAt()) * fm.width(QLatin1Char('x')) - xStart; paint.drawLine(_x, 0, _x, lineHeight()); paint.setRenderHints(backupRenderHints); } } const QFont &KateRenderer::currentFont() const { return config()->font(); } const QFontMetricsF &KateRenderer::currentFontMetrics() const { return config()->fontMetrics(); } uint KateRenderer::fontHeight() const { return m_fontHeight; } uint KateRenderer::documentHeight() const { return m_doc->lines() * lineHeight(); } int KateRenderer::lineHeight() const { return fontHeight(); } bool KateRenderer::getSelectionBounds(int line, int lineLength, int &start, int &end) const { bool hasSel = false; if (m_view->selection() && !m_view->blockSelection()) { if (m_view->lineIsSelection(line)) { start = m_view->selectionRange().start().column(); end = m_view->selectionRange().end().column(); hasSel = true; } else if (line == m_view->selectionRange().start().line()) { start = m_view->selectionRange().start().column(); end = lineLength; hasSel = true; } else if (m_view->selectionRange().containsLine(line)) { start = 0; end = lineLength; hasSel = true; } else if (line == m_view->selectionRange().end().line()) { start = 0; end = m_view->selectionRange().end().column(); hasSel = true; } } else if (m_view->lineHasSelected(line)) { start = m_view->selectionRange().start().column(); end = m_view->selectionRange().end().column(); hasSel = true; } if (start > end) { int temp = end; end = start; start = temp; } return hasSel; } void KateRenderer::updateConfig() { // update the attribute list pointer updateAttributes(); // update font height, do this before we update the view! updateFontHeight(); // trigger view update, if any! if (m_view) { m_view->updateRendererConfig(); } } void KateRenderer::updateFontHeight() { - // ensure minimal height of one pixel to not fall in the div by 0 trap somewhere - // use font line spacing, this includes the leading - m_fontHeight = qMax(1, qCeil(config()->fontMetrics().lineSpacing())); + /** + * ensure minimal height of one pixel to not fall in the div by 0 trap somewhere + * + * use a line spacing that matches the code in qt to layout/paint text + * + * see bug 403868 + * https://github.com/qt/qtbase/blob/5.12/src/gui/text/qtextlayout.cpp (line 2270 at the moment) where the text height is set as: + * + * qreal height = maxY + fontHeight - minY; + * + * with fontHeight: + * + * qreal fontHeight = font.ascent() + font.descent(); + */ + m_fontHeight = qMax(1, qCeil(config()->fontMetrics().ascent() + config()->fontMetrics().descent())); } void KateRenderer::updateMarkerSize() { // marker size will be calculated over the value defined // on dialog m_markerSize = spaceWidth() / (3.5 - (m_doc->config()->markerSize() * 0.5)); } qreal KateRenderer::spaceWidth() const { return config()->fontMetrics().width(spaceChar); } void KateRenderer::layoutLine(KateLineLayoutPtr lineLayout, int maxwidth, bool cacheLayout) const { // if maxwidth == -1 we have no wrap Kate::TextLine textLine = lineLayout->textLine(); Q_ASSERT(textLine); QTextLayout *l = lineLayout->layout(); if (!l) { l = new QTextLayout(textLine->string(), config()->font()); } else { l->setText(textLine->string()); l->setFont(config()->font()); } l->setCacheEnabled(cacheLayout); // Initial setup of the QTextLayout. // Tab width QTextOption opt; opt.setFlags(QTextOption::IncludeTrailingSpaces); opt.setTabStop(m_tabWidth * config()->fontMetrics().width(spaceChar)); opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); // Find the first strong character in the string. // If it is an RTL character, set the base layout direction of the string to RTL. // // See http://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3). // Qt's text renderer ("scribe") version 4.2 assumes a "higher-level protocol" // (such as KatePart) will specify the paragraph level, so it does not apply P2 & P3 // by itself. If this ever change in Qt, the next code block could be removed. if (isLineRightToLeft(lineLayout)) { opt.setAlignment(Qt::AlignRight); opt.setTextDirection(Qt::RightToLeft); } else { opt.setAlignment(Qt::AlignLeft); opt.setTextDirection(Qt::LeftToRight); } l->setTextOption(opt); // Syntax highlighting, inbuilt and arbitrary QVector decorations = decorationsForLine(textLine, lineLayout->line()); int firstLineOffset = 0; if (!isPrinterFriendly()) { const auto inlineNotes = m_view->inlineNotes(lineLayout->line()); for (const KTextEditor::InlineNote& inlineNote: inlineNotes) { const int column = inlineNote.position().column(); int width = inlineNote.width(); // Make space for every inline note. // If it is on column 0 (at the beginning of the line), we must offset the first line. // If it is inside the text, we use absolute letter spacing to create space for it between the two letters. // If it is outside of the text, we don't have to make space for it. if (column == 0) { firstLineOffset = width; } else if (column < l->text().length()) { QTextCharFormat text_char_format; text_char_format.setFontLetterSpacing(width); text_char_format.setFontLetterSpacingType(QFont::AbsoluteSpacing); decorations.append(QTextLayout::FormatRange { column - 1, 1, text_char_format }); } } } l->setFormats(decorations); // Begin layouting l->beginLayout(); int height = 0; int shiftX = 0; bool needShiftX = (maxwidth != -1) && m_view && (m_view->config()->dynWordWrapAlignIndent() > 0); forever { QTextLine line = l->createLine(); if (!line.isValid()) { break; } if (maxwidth > 0) { line.setLineWidth(maxwidth); } // we include the leading, this must match the ::updateFontHeight code! line.setLeadingIncluded(true); line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height)); if (needShiftX && line.width() > 0) { needShiftX = false; // Determine x offset for subsequent-lines-of-paragraph indenting int pos = textLine->nextNonSpaceChar(0); if (pos > 0) { shiftX = (int)line.cursorToX(pos); } // check for too deep shift value and limit if necessary if (shiftX > ((double)maxwidth / 100 * m_view->config()->dynWordWrapAlignIndent())) { shiftX = 0; } // if shiftX > 0, the maxwidth has to adapted maxwidth -= shiftX; lineLayout->setShiftX(shiftX); } height += lineHeight(); } l->endLayout(); lineLayout->setLayout(l); } // 1) QString::isRightToLeft() sux // 2) QString::isRightToLeft() is marked as internal (WTF?) // 3) QString::isRightToLeft() does not seem to work on my setup // 4) isStringRightToLeft() should behave much better than QString::isRightToLeft() therefore: // 5) isStringRightToLeft() kicks ass bool KateRenderer::isLineRightToLeft(KateLineLayoutPtr lineLayout) const { QString s = lineLayout->textLine()->string(); int i = 0; // borrowed from QString::updateProperties() while (i != s.length()) { QChar c = s.at(i); switch (c.direction()) { case QChar::DirL: case QChar::DirLRO: case QChar::DirLRE: return false; case QChar::DirR: case QChar::DirAL: case QChar::DirRLO: case QChar::DirRLE: return true; default: break; } i ++; } return false; #if 0 // or should we use the direction of the widget? QWidget *display = qobject_cast(view()->parent()); if (!display) { return false; } return display->layoutDirection() == Qt::RightToLeft; #endif } int KateRenderer::cursorToX(const KateTextLayout &range, int col, bool returnPastLine) const { return cursorToX(range, KTextEditor::Cursor(range.line(), col), returnPastLine); } int KateRenderer::cursorToX(const KateTextLayout &range, const KTextEditor::Cursor &pos, bool returnPastLine) const { Q_ASSERT(range.isValid()); int x; if (range.lineLayout().width() > 0) { x = (int)range.lineLayout().cursorToX(pos.column()); } else { x = 0; } int over = pos.column() - range.endCol(); if (returnPastLine && over > 0) { x += over * spaceWidth(); } return x; } KTextEditor::Cursor KateRenderer::xToCursor(const KateTextLayout &range, int x, bool returnPastLine) const { Q_ASSERT(range.isValid()); KTextEditor::Cursor ret(range.line(), range.lineLayout().xToCursor(x)); // TODO wrong for RTL lines? if (returnPastLine && range.endCol(true) == -1 && x > range.width() + range.xOffset()) { ret.setColumn(ret.column() + ((x - (range.width() + range.xOffset())) / spaceWidth())); } return ret; } void KateRenderer::setCaretOverrideColor(const QColor &color) { m_caretOverrideColor = color; } diff --git a/src/view/kateviewinternal.cpp b/src/view/kateviewinternal.cpp index ca071710..c94863cd 100644 --- a/src/view/kateviewinternal.cpp +++ b/src/view/kateviewinternal.cpp @@ -1,3881 +1,3872 @@ /* 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 } 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(); - int sy = startz * h; + /** + * 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.save(); - + /** + * paint regions without lines mapped to + */ if ((z >= lineRangesSize) || (cache()->viewLine(z).line() == -1)) { if (!(z >= lineRangesSize)) { cache()->viewLine(z).setDirty(false); } - - // set clipping region if we paint the line above us, too - paint.setClipRect(QRect(0, 0, unionRect.width(), h)); paint.fillRect(0, 0, unionRect.width(), h, renderer()->config()->backgroundColor()); + } - // second: paint previous line elements, that span into our line like _ in last line, bug 335079 - if (z > 0 && (z-1 < lineRangesSize) && (cache()->viewLine(z-1).line() != -1)) { - KateTextLayout &previousLine = cache()->viewLine(z-1); - paint.save(); - paint.translate(QPoint(0, h * - (previousLine.viewLine() + 1))); - renderer()->paintTextLine(paint, previousLine.kateLineLayout(), xStart, xEnd, &pos); - paint.restore(); - } - } else { - //qCDebug(LOG_KTE)<<"KateViewInternal::paintEvent(QPaintEvent *e):cache()->viewLine"<viewLine(z); - - /* 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. + /** + * 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) { - //qDebug() << "paint text: line: " << thisLine.line() << " viewLine " << thisLine.viewLine() << " x: " << unionRect.x() << " y: " << unionRect.y() << " width: " << xEnd-xStart << " height: " << h << endl; - - // first: paint our line - // set clipping region if we paint the line above us, too + /** + * 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.translate(0, h * thisLine.viewLine()); - - // second: paint previous line elements, that span into our line like _, bug 335079 - if (z > 0) { - KateTextLayout &previousLine = cache()->viewLine(z-1); - paint.save(); - paint.translate(QPoint(0, h * - (previousLine.viewLine() + 1))); - renderer()->paintTextLine(paint, previousLine.kateLineLayout(), xStart, xEnd, &pos); - paint.restore(); - } + paint.restore(); /** * line painted, reset and state + mark line as non-dirty */ thisLine.setDirty(false); } } - paint.restore(); + /** + * translate to next line + */ paint.translate(0, h); - sy += 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); 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; } // 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 if (event->dropAction() != Qt::CopyAction) { editSetCursor(view()->selectionRange().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 (!view()->blockSelection()) { endCursor1.move(textLength); } else { endCursor1.setColumn(startCursor.column() + selectionWidth); endCursor1.setLine(startCursor.line() + selectionHeight); } KTextEditor::Cursor endCursor(endCursor1); qCDebug(LOG_KTE) << startCursor << "---(" << textLength << ")---" << endCursor; setSelection(KTextEditor::Range(startCursor, endCursor)); editSetCursor(endCursor); 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 {}; }