diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,12 @@ cmake_minimum_required(VERSION 3.0) set(KF5_VERSION "5.50.0") # handled by release scripts -set(KF5_DEP_VERSION "5.49.0") # handled by release scripts +set(KF5_DEP_VERSION "5.48.0") # handled by release scripts project(KTextEditor VERSION ${KF5_VERSION}) # ECM setup include(FeatureSummary) -find_package(ECM 5.49.0 NO_MODULE) +find_package(ECM 5.48.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/document/katedocument.h b/src/document/katedocument.h --- a/src/document/katedocument.h +++ b/src/document/katedocument.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include "katetextline.h" diff --git a/src/include/ktexteditor/inlinenoteinterface.h b/src/include/ktexteditor/inlinenoteinterface.h new file mode 100755 --- /dev/null +++ b/src/include/ktexteditor/inlinenoteinterface.h @@ -0,0 +1,272 @@ +/* This file is part of the KDE libraries + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + 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. +*/ +#ifndef KTEXTEDITOR_INLINENOTEINTERFACE_H +#define KTEXTEDITOR_INLINENOTEINTERFACE_H + +#include + +#include + +#include + +class QFont; +class QFontMetricsF; +class QPainter; + +namespace KTextEditor +{ + +class InlineNoteProvider; +class View; + +/** + * \brief Inline notes interface for rendering notes in the text. + * + * \ingroup kte_group_view_extensions + * + * \section inlinenote_introduction Introduction + * + * The inline notes interface provides a way to render arbitrary things in + * the text. The layout of the line is adapted to create space for the note. + * Possible applications include showing a name of a function parameter on + * call side or rendering square with color preview next to CSS color + * property. + * + * To register as inline note provider, call registerInlineNoteProvider() with + * an instance that inherits InlineNoteProvider. Finally, make sure you remove + * your inline note provider by calling unregisterInlineNoteProvider(). + * + * \section inlinenote_access Accessing the InlineNoteInterface + * + * The InlineNoteInterface is an extension interface for a + * View, i.e. the View inherits the interface. Use qobject_cast to access the + * interface: + * \code + * // view is of type KTextEditor::View* + * KTextEditor::InlineNoteInterface *iface = + * qobject_cast(view); + * + * if (iface) { + * // the implementation supports the interface + * // myProvider inherits KTextEditor::InlineNoteProvider + * iface->registerInlineNoteProvider(myProvider); + * } + * \endcode + * + * \see InlineNoteProvider + */ +class KTEXTEDITOR_EXPORT InlineNoteInterface +{ +public: + InlineNoteInterface(); + virtual ~InlineNoteInterface(); + + /** + * Register the inline note provider \p provider. + * + * Whenever a line is painted, the \p provider will be queried for notes + * that should be painted in it. When the provider is about to be + * destroyed, make sure to call unregisterTextHintProvider() to avoid a + * dangling pointer. + * + * \param provider inline note provider + * \see unregisterInlineNoteProvider(), InlineNoteProvider + */ + virtual void registerInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) = 0; + + /** + * Unregister the inline note provider \p provider. + * + * \param provider inline note provider to unregister + * \see registerInlineNoteProvider(), InlineNoteProvider + */ + virtual void unregisterInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) = 0; +}; + +class InlineNote; + +/** + * \brief A source of inline notes for a document. + * + * InlineNoteProvider is object that can be queried for inline notes in the + * document. It emits signals when the notes change and should be queried again. + * + * \see InlineNoteInterface + */ +class KTEXTEDITOR_EXPORT InlineNoteProvider : public QObject +{ + Q_OBJECT + +public: + enum ActivationType { + Inactive, ///< The note is inactive (the default) + Focus, ///< The note has received focus or has focus + Click, ///< The note was clicked + ShortcutActivate, ///< The note was activated by a shortcut + }; + + /** + * Default constructor. + */ + InlineNoteProvider(); + + /** + * Virtual destructor to allow inheritance. + */ + virtual ~InlineNoteProvider(); + + /** + * Get list of inline notes for given line. + * + * Should return a vector of columns on which the notes are located. + * 0 means the note is located before the first character of the line. + * 1 means the note is located after the first character, etc. If the + * returned number is bigger than the length of the line, the note will be + * placed behind the text as if there were additional spaces. + * + * \param line Line number + * \returns vector of columns where inline notes appear in this line + */ + virtual QVector inlineNotes(int line) const = 0; + + /** + * Width to be reserved for the note in the text. + * + * The method is given the height of the line and the metrics of current + * font which it may use for calculating the width. + * + * \param height the height of the line in pixels + * \param font the font used by the editor + * + * \return the width of the note in pixels + */ + virtual QSize inlineNoteSize(const InlineNote& note, qreal height, QFont font) const = 0; + + /** + * Paint the note into the line. + * + * The method should use the given painter to render the note into the + * line. The painter is translated such that coordinates 0x0 mark the top + * left corner of the note. The method should not paint outside rectangle + * given by the height parameter and the width previously returned by the + * width method. + * + * The method is given the height of the line, the metrics of current font + * and the font which it may use during painting. + * + * \param note note to paint, containing location and index + * \param height the height of the line in pixels + * \param font the QFont used in the editor + * \param painter painter prepared for rendering the note + */ + virtual void paintInlineNote(const InlineNote& note, qreal height, QFont font, QPainter& painter) const = 0; + + /** + * Invoked when a note is activated by the user. + * + * This method is called when a user activates a note, i.e. clicks on it or otherwise + * interacts with it. Coordinates of \p pos are relative to the note's + * top-left corner (same coordinate system as the painter has in paintInlineNote()). + * + * In case of a focus-out event, this method is called with \p type set to Inactive. + * + * The default implementation does nothing. + * + * \param note the note which was activated + * \param type The type of activation + * \param pos the point the note was clicked at, or {-1,-1} if \p type is ShortcutActivate + */ + virtual void noteActivated(const InlineNote& note, ActivationType type, QPoint pos) { + Q_UNUSED(note); + Q_UNUSED(type); + Q_UNUSED(pos); + } + +Q_SIGNALS: + /** + * The provider should emit the signal reset() when almost all inline notes + * changed. + */ + void reset(); + + /** + * The provider should emit the signal lineChanged() when any of the + * inline notes on the line changed. + */ + void lineChanged(int line); +}; + +class KTEXTEDITOR_EXPORT InlineNote +{ +public: + InlineNote(InlineNoteProvider* provider, const KTextEditor::Cursor& location, int index) + : provider(provider) + , location(location) + , index(index) + , activeState(InlineNoteProvider::Inactive) + { + } + + InlineNote() + : provider(nullptr) + , location({}) + , index(-1) + , activeState(InlineNoteProvider::Inactive) + { + } + + int column() const { + return location.column(); + } + + qreal width(qreal height, const QFont &font) const { + return provider->inlineNoteSize(*this, height, font).width(); + } + + void paint(qreal height, const QFont &font, QPainter &painter) const { + provider->paintInlineNote(*this, height, font, painter); + } + + void activate(InlineNoteProvider::ActivationType type, QPoint pos) { + provider->noteActivated(*this, type, pos); + } + + bool isValid() const { + return provider != nullptr && location.isValid(); + } + + operator bool() const { + return isValid(); + } + + // Does not compare activeState. + bool operator==(const InlineNote& other) const { + return provider == other.provider && location == other.location && index == other.index; + } + + InlineNoteProvider* provider; + KTextEditor::Cursor location; + int index; + InlineNoteProvider::ActivationType activeState; +}; + +} + +Q_DECLARE_INTERFACE(KTextEditor::InlineNoteInterface, "org.kde.KTextEditor.InlineNoteInterface") + +#endif diff --git a/src/render/katerenderer.h b/src/render/katerenderer.h --- a/src/render/katerenderer.h +++ b/src/render/katerenderer.h @@ -35,6 +35,7 @@ namespace KTextEditor { class DocumentPrivate; } namespace KTextEditor { class ViewPrivate; } +namespace KTextEditor { class InlineNote; } class KateRendererConfig; class KateRenderRange; namespace Kate diff --git a/src/render/katerenderer.cpp b/src/render/katerenderer.cpp --- a/src/render/katerenderer.cpp +++ b/src/render/katerenderer.cpp @@ -31,8 +31,11 @@ #include "katetextlayout.h" #include "katebuffer.h" +#include "ktexteditor/inlinenoteinterface.h" + #include "katepartdebug.h" +#include #include #include #include @@ -758,6 +761,36 @@ } } + // Draw inline notes + auto inlineNotes = m_view->inlineNotes(range->line()); + foreach (const KTextEditor::InlineNote& inlineNote, inlineNotes) { + int column = inlineNote.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(lineHeight(), currentFont()); + } 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.paint(lineHeight(), currentFont(), paint); + paint.restore(); + } + // draw word-wrap-honor-indent filling if ((range->viewLineCount() > 1) && range->shiftX() && (range->shiftX() > xStart)) { if (backgroundBrushSet) @@ -1008,7 +1041,30 @@ l->setTextOption(opt); // Syntax highlighting, inbuilt and arbitrary - l->setAdditionalFormats(decorationsForLine(textLine, lineLayout->line())); + QList decorations = decorationsForLine(textLine, lineLayout->line()); + + int firstLineOffset = 0; + + auto inlineNotes = m_view->inlineNotes(lineLayout->line()); + foreach (const KTextEditor::InlineNote& inlineNote, inlineNotes) { + int column = inlineNote.column(); + int width = inlineNote.width(lineHeight(), currentFont()); + + // 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->setAdditionalFormats(decorations); // Begin layouting l->beginLayout(); @@ -1034,7 +1090,7 @@ // we include the leading, this must match the ::updateFontHeight code! line.setLeadingIncluded(true); - line.setPosition(QPoint(line.lineNumber() ? shiftX : 0, height)); + line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height)); if (needShiftX && line.width() > 0) { diff --git a/src/utils/ktexteditor.cpp b/src/utils/ktexteditor.cpp --- a/src/utils/ktexteditor.cpp +++ b/src/utils/ktexteditor.cpp @@ -32,6 +32,7 @@ #include "plugin.h" #include "command.h" +#include "inlinenoteinterface.h" #include "markinterface.h" #include "modificationinterface.h" #include "sessionconfiginterface.h" @@ -210,6 +211,18 @@ TextHintProvider::~TextHintProvider() {} +InlineNoteInterface::InlineNoteInterface() +{} + +InlineNoteInterface::~InlineNoteInterface() +{} + +InlineNoteProvider::InlineNoteProvider() +{} + +InlineNoteProvider::~InlineNoteProvider() +{} + Command::Command(const QStringList &cmds, QObject *parent) : QObject(parent) , m_cmds (cmds) diff --git a/src/view/kateview.h b/src/view/kateview.h --- a/src/view/kateview.h +++ b/src/view/kateview.h @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -84,13 +85,15 @@ public KTextEditor::TextHintInterface, public KTextEditor::CodeCompletionInterface, public KTextEditor::ConfigInterface, + public KTextEditor::InlineNoteInterface, public KTextEditor::AnnotationViewInterface { Q_OBJECT Q_INTERFACES(KTextEditor::TextHintInterface) Q_INTERFACES(KTextEditor::ConfigInterface) Q_INTERFACES(KTextEditor::CodeCompletionInterface) Q_INTERFACES(KTextEditor::AnnotationViewInterface) + Q_INTERFACES(KTextEditor::InlineNoteInterface) friend class KTextEditor::View; friend class ::KateViewInternal; @@ -250,6 +253,22 @@ return m_hasWrap; } + // + // Inline Notes Interface + // +public: + void registerInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) Q_DECL_OVERRIDE; + void unregisterInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) Q_DECL_OVERRIDE; + + QVarLengthArray inlineNotes(int line) const; + +private: + QVector m_inlineNoteProviders; + +private Q_SLOTS: + void inlineNotesReset(); + void inlineNotesLineChanged(int line); + // // KTextEditor::SelectionInterface stuff // diff --git a/src/view/kateview.cpp b/src/view/kateview.cpp --- a/src/view/kateview.cpp +++ b/src/view/kateview.cpp @@ -3644,6 +3644,70 @@ //END +//BEGIN KTextEditor::InlineNoteInterface +void KTextEditor::ViewPrivate::registerInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) +{ + if (! m_inlineNoteProviders.contains(provider)) { + m_inlineNoteProviders.append(provider); + + connect(provider, &KTextEditor::InlineNoteProvider::reset, this, &ViewPrivate::inlineNotesReset); + connect(provider, &KTextEditor::InlineNoteProvider::lineChanged, this, &ViewPrivate::inlineNotesLineChanged); + + inlineNotesReset(); + } +} + +void KTextEditor::ViewPrivate::unregisterInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) +{ + const int index = m_inlineNoteProviders.indexOf(provider); + if (index >= 0) { + m_inlineNoteProviders.removeAt(index); + + disconnect(provider, nullptr, this, nullptr); + + inlineNotesReset(); + } +} + +QVarLengthArray KTextEditor::ViewPrivate::inlineNotes(int line) const +{ + QVarLengthArray allInlineNotes; + for (KTextEditor::InlineNoteProvider *provider: m_inlineNoteProviders) { + int index = 0; + for (auto column: provider->inlineNotes(line)) { + KTextEditor::InlineNote note = { + provider, + {line, column}, + index + }; + auto activeNote = m_viewInternal->m_activeInlineNote; + if ( note == activeNote ) { + note.activeState = activeNote.activeState; + } + allInlineNotes.append(note); + index++; + } + } + return allInlineNotes; +} + +void KTextEditor::ViewPrivate::inlineNotesReset() +{ + m_viewInternal->m_activeInlineNote = {}; + repaintText(false); +} + +void KTextEditor::ViewPrivate::inlineNotesLineChanged(int line) +{ + if ( line == m_viewInternal->m_activeInlineNote.location.line() ) { + m_viewInternal->m_activeInlineNote = {}; + } + tagLines(line, line); + repaintText(true); +} + +//END KTextEditor::InlineNoteInterface + KTextEditor::Attribute::Ptr KTextEditor::ViewPrivate::defaultStyleAttribute(KTextEditor::DefaultStyle defaultStyle) const { KateRendererConfig * renderConfig = const_cast(this)->renderer()->config(); diff --git a/src/view/kateviewinternal.h b/src/view/kateviewinternal.h --- a/src/view/kateviewinternal.h +++ b/src/view/kateviewinternal.h @@ -466,6 +466,11 @@ private: QMap m_inputModes; KateAbstractInputMode *m_currentInputMode; + + KTextEditor::InlineNote m_activeInlineNote; + KTextEditor::InlineNote inlineNoteAt(const QPoint& pos) const; + QRect inlineNoteRect(const KTextEditor::InlineNote& note) const; + QPoint toNoteCoordinates(const QPoint& pos, const KTextEditor::InlineNote& note) const; }; #endif diff --git a/src/view/kateviewinternal.cpp b/src/view/kateviewinternal.cpp --- a/src/view/kateviewinternal.cpp +++ b/src/view/kateviewinternal.cpp @@ -2589,6 +2589,11 @@ { switch (e->button()) { case Qt::LeftButton: + if (auto note = inlineNoteAt(e->pos())) { + note.activate(KTextEditor::InlineNoteProvider::Click, toNoteCoordinates(e->pos(), note)); + return; + } + m_selChangedByUser = false; if (m_possibleTripleClick) { @@ -2875,6 +2880,23 @@ mouseMoved(); } + if (e->buttons() == Qt::NoButton) { + if (auto note = inlineNoteAt(e->pos())) { + const auto newState = KTextEditor::InlineNoteProvider::Focus; + note.activate(newState, toNoteCoordinates(e->pos(), note)); + m_activeInlineNote = note; + m_activeInlineNote.activeState = newState; + tagLine(note.location); + m_view->repaintText(true); + } + else if (m_activeInlineNote) { + m_activeInlineNote.activate(KTextEditor::InlineNoteProvider::Inactive, {}); + tagLine(m_activeInlineNote.location); + m_activeInlineNote = {}; + m_view->repaintText(true); + } + } + if (e->buttons() & Qt::LeftButton) { if (m_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet @@ -3857,3 +3879,42 @@ } #endif } + +QRect KateViewInternal::inlineNoteRect(const KTextEditor::InlineNote& note) const +{ + // compute note width and position + auto noteWidth = note.width(renderer()->lineHeight(), renderer()->currentFont()); + auto noteCursor = KTextEditor::Cursor(note.location.line(), note.column()); + + // 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 = 0; + if (noteCursor.column() > lineLength) { + extraOffset = (noteCursor.column() - lineLength) * renderer()->spaceWidth(); + noteCursor.setColumn(lineLength); + } + auto noteStartPos = cursorToCoordinate(noteCursor, true, false); + + // compute the note's rect + auto noteRect = QRect(noteStartPos + QPoint{extraOffset, 0}, QSize(noteWidth, renderer()->lineHeight())); + return noteRect; +} + +KTextEditor::InlineNote KateViewInternal::inlineNoteAt(const QPoint& pos) const +{ + auto cursor = m_view->coordinatesToCursor(pos); + auto inlineNotes = m_view->inlineNotes(cursor.line()); + foreach (const auto& note, inlineNotes) { + auto noteRect = inlineNoteRect(note); + if (noteRect.contains(pos)) { + return note; + } + } + return {}; +} + +QPoint KateViewInternal::toNoteCoordinates(const QPoint& pos, const KTextEditor::InlineNote& note) const +{ + return pos - inlineNoteRect(note).topLeft(); +}