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/CMakeLists.txt b/src/include/CMakeLists.txt --- a/src/include/CMakeLists.txt +++ b/src/include/CMakeLists.txt @@ -2,7 +2,7 @@ ecm_generate_headers(KTextEditor_CamelCase_HEADERS HEADER_NAMES AnnotationInterface CodeCompletionModelControllerInterface MovingCursor Range TextHintInterface - Cursor MarkInterface MovingInterface + Cursor MarkInterface MovingInterface InlineNoteInterface Document MovingRange View Attribute Command DocumentCursor Message MovingRangeFeedback SessionConfigInterface CodeCompletionInterface ConfigInterface Editor 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,331 @@ +/* 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 +#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) 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, 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: + /** + * Constructs an inline note. User code usually does not need to call this, + * notes are created from the columns returned by InlineNoteProvider::inlineNotes(int line), + * and then passed around as handles grouping useful information. + */ + InlineNote(InlineNoteProvider* provider, const KTextEditor::Cursor& location, int index, + const KTextEditor::View* view, QFont font, int lineHeight) + : provider(provider) + , view(view) + , location(location) + , index(index) + , activeState(InlineNoteProvider::Inactive) + , font(font) + , lineHeight(lineHeight) + { + } + + /** + * Constructs an invalid inline note, i.e. isValid() will return false. + */ + InlineNote() + : provider(nullptr) + , view(nullptr) + , location({}) + , index(-1) + , activeState(InlineNoteProvider::Inactive) + { + } + + /** + * Returns the column this note appears in. + */ + int column() const { + return location.column(); + } + + /** + * Returns the width of this note in pixels. + */ + qreal width() const { + return provider->inlineNoteSize(*this).width(); + } + + /** + * Paints this note with the given @p painter. + */ + void paint(QPainter &painter) const { + provider->paintInlineNote(*this, painter); + } + + /** + * Activates this note, notifying the provider of an event. + */ + void activate(InlineNoteProvider::ActivationType type, QPoint pos) { + provider->noteActivated(*this, type, pos); + } + + /** + * Tells whether this note is valid, i.e. whether it has a valid provider and location set. + */ + bool isValid() const { + return provider != nullptr && location.isValid(); + } + + /** + * @see isValid + */ + operator bool() const { + return isValid(); + } + + /** + * Equality of notes. Only checks provider, index, and location. + */ + bool operator==(const InlineNote& other) const { + return provider == other.provider && location == other.location && index == other.index; + } + + /** + * Transforms the given @p pos from note coordinates to global (screen) coordinates. + * + * Useful for showing a popup; to e.g. show a popup at the bottom left corner + * of a note, show it at \code mapToGlobal({0, noteHeight}) \endcode. + */ + QPoint mapToGlobal(const QPoint& pos) const; + + /// The provider which created this note + InlineNoteProvider* provider; + + /// The view this note is shown in + const KTextEditor::View* view; + + /// The location of this note + KTextEditor::Cursor location; + + /// The index of this note, i.e. its index in the vector returned by + /// the provider for a given line + int index; + + /// The state of this note, only set for paint() + InlineNoteProvider::ActivationType activeState; + + /// The font of the text surrounding this note + QFont font; + + /// The height of the line containing this note + int lineHeight; +}; + +} + +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(); + } 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(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(); + + // 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,24 @@ TextHintProvider::~TextHintProvider() {} +InlineNoteInterface::InlineNoteInterface() +{} + +InlineNoteInterface::~InlineNoteInterface() +{} + +InlineNoteProvider::InlineNoteProvider() +{} + +InlineNoteProvider::~InlineNoteProvider() +{} + +QPoint InlineNote::mapToGlobal(const QPoint& pos) const +{ + auto localPos = static_cast(view)->inlineNoteRect(*this).topLeft() + pos; + return view->mapToGlobal(localPos); +} + 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,23 @@ return m_hasWrap; } + // + // Inline Notes Interface + // +public: + void registerInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) Q_DECL_OVERRIDE; + void unregisterInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) Q_DECL_OVERRIDE; + QRect inlineNoteRect(const KTextEditor::InlineNote& note) const; + + 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,78 @@ //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, + this, + m_viewInternal->renderer()->currentFont(), + m_viewInternal->renderer()->lineHeight(), + }; + auto activeNote = m_viewInternal->m_activeInlineNote; + if ( note == activeNote ) { + note.activeState = activeNote.activeState; + } + allInlineNotes.append(note); + index++; + } + } + return allInlineNotes; +} + +QRect KTextEditor::ViewPrivate::inlineNoteRect(const KTextEditor::InlineNote& note) const +{ + return m_viewInternal->inlineNoteRect(note); +} + +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,45 @@ } #endif } + +QRect KateViewInternal::inlineNoteRect(const KTextEditor::InlineNote& note) const +{ + // compute note width and position + auto noteWidth = note.width(); + 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 +{ + // compute the associated cursor to get the right line + auto cursor = m_view->coordinatesToCursor(pos); + auto inlineNotes = m_view->inlineNotes(cursor.line()); + // loop over all notes and check if the point is inside it + foreach (const auto& note, inlineNotes) { + auto noteRect = inlineNoteRect(note); + if (noteRect.contains(pos)) { + return note; + } + } + // none found -- return an invalid note + return {}; +} + +QPoint KateViewInternal::toNoteCoordinates(const QPoint& pos, const KTextEditor::InlineNote& note) const +{ + return pos - inlineNoteRect(note).topLeft(); +}