diff --git a/share/qtcreator/lua-plugins/luatests/tests.lua b/share/qtcreator/lua-plugins/luatests/tests.lua index 213aff47ebf..5ab9f8ccca0 100644 --- a/share/qtcreator/lua-plugins/luatests/tests.lua +++ b/share/qtcreator/lua-plugins/luatests/tests.lua @@ -77,6 +77,7 @@ local function setup() script()() end, }) + require 'tst_texteditor'.setup() end return { setup = setup } diff --git a/share/qtcreator/lua-plugins/luatests/tst_texteditor.lua b/share/qtcreator/lua-plugins/luatests/tst_texteditor.lua new file mode 100644 index 00000000000..a33c6bfe783 --- /dev/null +++ b/share/qtcreator/lua-plugins/luatests/tst_texteditor.lua @@ -0,0 +1,61 @@ +local function tst_embedWidget() + T = require 'TextEditor' + G = require 'Gui' + + local editor = T.currentEditor() + if not editor then + print("No editor found") + return + end + + local cursor = editor:cursor() + + local embed + local optionals = G.Group { + visible = false, + G.Row { + G.Label { + text = "Optional 1", + }, + G.Label { + text = "Optional 2", + }, + } + } + + local layout = G.Group { + G.Column { + "Hello", G.br, + "World", + G.br, + G.PushButton { + text = "Show optionals", + onClicked = function() + optionals.visible = not optionals.visible + embed:resize() + end, + }, + optionals, + G.PushButton { + text = "Close", + onClicked = function() + embed:close() + end, + }, + } + } + + embed = editor:addEmbeddedWidget(layout, cursor:mainCursor():position()) +end + +local function setup() + Action = require 'Action' + Action.create("LuaTests.textEditorEmbedDemo", { + text = "Lua TextEditor Embed Demo", + onTrigger = tst_embedWidget, + }) +end + +return { + setup = setup, +} diff --git a/src/plugins/lua/bindings/texteditor.cpp b/src/plugins/lua/bindings/texteditor.cpp index c4f5977d6b1..2ba53d8ca68 100644 --- a/src/plugins/lua/bindings/texteditor.cpp +++ b/src/plugins/lua/bindings/texteditor.cpp @@ -46,65 +46,14 @@ TextEditor::TextEditorWidget *getSuggestionReadyEditorWidget(TextEditor::TextDoc return widget; } -void fillRemainingViewportWidth(QWidget *widget, const QSize &viewportSize, const QMargins &margins) +std::unique_ptr addEmbeddedWidget( + BaseTextEditor *editor, QWidget *widget, int cursorPosition) { - int maxWidth = viewportSize.width() - margins.right() - widget->x(); - widget->setFixedWidth(maxWidth); -} - -QPoint getPositionOnViewport(const BaseTextEditor * const editor, const QWidget * const widget, - int basePos, int xPos, const QSize &viewportSize, - const QMargins &margins) -{ - QTextCursor cursor = QTextCursor(editor->textDocument()->document()); - cursor.setPosition(basePos); - - const QRect cursorRect = editor->editorWidget()->cursorRect(cursor); - QPoint widgetPosDefault = cursorRect.bottomLeft(); - - widgetPosDefault.ry() += (cursorRect.top() - cursorRect.bottom()) / 2; - - int fontSize = editor->textDocument()->fontSettings().fontSize(); - int maxX = viewportSize.width() - margins.right() - widget->width(); - int maxY = viewportSize.height() - widget->height() - fontSize; - - if (maxX < 0) { - qWarning() << QStringLiteral("Floating Widget positioning: x (%1) < 0. Widget will not " - "fit in the viewport. Viewport.width (%2), widget.width (%3), " - "widget margin.right (%4). Setting x to 0.") - .arg(maxX).arg(viewportSize.width()).arg(widget->width()).arg(margins.right()); - maxX = 0; - } - - if (maxY < 0) { - qWarning() << QStringLiteral("Floating Widget positioning: y (%1) < 0. Widget is too big" - "for the viewport. Viewport.height (%2), widget.height (%3)." - "Setting y to 0.") - .arg(maxY).arg(viewportSize.height()).arg(widget->height()); - maxY = 0; - } - - int x = xPos != -1 ? xPos : std::min(widgetPosDefault.x(), maxX); - int y = widgetPosDefault.y() + fontSize; - y = std::min(y, maxY); - - return {x, y}; -} - -void addFloatingWidget(BaseTextEditor *editor, QWidget *widget, int yPos, int xPos, - const QRect &margins, bool fillWidth = false) -{ - QMargins widgetMargins{margins.left(), margins.top(), margins.width(), margins.height()}; - widget->setParent(editor->editorWidget()->viewport()); TextEditorWidget *editorWidget = editor->editorWidget(); - const QSize viewportSize = editorWidget->viewport()->size(); - - widget->move(getPositionOnViewport(editor, widget, yPos, xPos, viewportSize, widgetMargins)); - if (fillWidth) - fillRemainingViewportWidth(widget, viewportSize, widgetMargins); - - widget->show(); + std::unique_ptr embed + = editorWidget->insertWidget(widget, cursorPosition); + return embed; } } // namespace @@ -213,9 +162,7 @@ void setupTextEditorModule() "cursors", [](MultiTextCursor *self) { return sol::as_table(self->cursors()); }, "insertText", - [](MultiTextCursor *self, const QString &text) { - self->insertText(text); - }); + [](MultiTextCursor *self, const QString &text) { self->insertText(text); }); result.new_usertype( "Position", @@ -273,9 +220,33 @@ void setupTextEditorModule() return ret; }, "insertText", - [](QTextCursor *textCursor, const QString &text) { - textCursor->insertText(text); - }); + [](QTextCursor *textCursor, const QString &text) { textCursor->insertText(text); }); + + using LayoutOrWidget = std::variant; + + static auto toWidget = [](LayoutOrWidget &arg) { + return std::visit( + [](auto &&arg) -> QWidget * { + using T = std::decay_t; + if constexpr (std::is_same_v) + return arg->emerge(); + else if constexpr (std::is_same_v) + return arg; + else if constexpr (std::is_same_v) + return arg->emerge(); + else + return nullptr; + }, + arg); + }; + + result.new_usertype( + "EmbeddedWidgetInterface", + sol::no_constructor, + "resize", + &EmbeddedWidgetInterface::resize, + "close", + &EmbeddedWidgetInterface::close); result.new_usertype( "TextEditor", @@ -285,23 +256,11 @@ void setupTextEditorModule() QTC_ASSERT(textEditor, throw sol::error("TextEditor is not valid")); return textEditor->textDocument(); }, - "addFloatingWidget", - sol::overload( - [](const TextEditorPtr &textEditor, QWidget *widget, int yPos, int xPos, - const QRect &margins, bool fillWidth) { - QTC_ASSERT(textEditor, throw sol::error("TextEditor is not valid")); - addFloatingWidget(textEditor, widget, yPos, xPos, margins, fillWidth); - }, - [](const TextEditorPtr &textEditor, Layouting::Widget *widget, int yPos, int xPos, - const QRect &margins, bool fillWidth) { - QTC_ASSERT(textEditor, throw sol::error("TextEditor is not valid")); - addFloatingWidget(textEditor, widget->emerge(), yPos, xPos, margins, fillWidth); - }, - [](const TextEditorPtr &textEditor, Layouting::Layout *layout, int yPos, int xPos, - const QRect &margins, bool fillWidth = false) { - QTC_ASSERT(textEditor, throw sol::error("TextEditor is not valid")); - addFloatingWidget(textEditor, layout->emerge(), yPos, xPos, margins, fillWidth); - }), + "addEmbeddedWidget", + [](const TextEditorPtr &textEditor, LayoutOrWidget widget, int position) { + QTC_ASSERT(textEditor, throw sol::error("TextEditor is not valid")); + return addEmbeddedWidget(textEditor, toWidget(widget), position); + }, "cursor", [](const TextEditorPtr &textEditor) { QTC_ASSERT(textEditor, throw sol::error("TextEditor is not valid")); diff --git a/src/plugins/lua/meta/gui.lua b/src/plugins/lua/meta/gui.lua index d16837c3c0e..c45f972f332 100644 --- a/src/plugins/lua/meta/gui.lua +++ b/src/plugins/lua/meta/gui.lua @@ -12,8 +12,8 @@ gui.layout = {} ---The base class of all widget classes, an empty widget itself. ---@class Widget : Object ----@field visible bool Whether the widget is visible or not. ----@field enabled bool Whether the widget is enabled or not. +---@field visible boolean Whether the widget is visible or not. +---@field enabled boolean Whether the widget is enabled or not. gui.widget = {} ---@alias LayoutChild string|BaseAspect|Layout|Widget|function @@ -168,7 +168,7 @@ local label = {} ---@class (exact) LabelOptions : BaseWidgetOptions ---@param interactionFlags? TextInteractionFlag[] ---@param textFormat? TextFormat The text format enum ----@param wordWrap? bool +---@param wordWrap? boolean gui.labelOptions = {} @@ -220,8 +220,8 @@ function gui.TabWidget(options) end function gui.TabWidget(name, child) end ---@class Spinner : Widget ----@field running bool Set spinner visible and display spinning animation ----@field decorated bool Display spinner with custom styleSheet defined inside control (default true) +---@field running boolean Set spinner visible and display spinning animation +---@field decorated boolean Display spinner with custom styleSheet defined inside control (default true) local spinner = {} ---@class IconDisplay : Widget diff --git a/src/plugins/lua/meta/texteditor.lua b/src/plugins/lua/meta/texteditor.lua index cb62a6e473d..2123b0a1fca 100644 --- a/src/plugins/lua/meta/texteditor.lua +++ b/src/plugins/lua/meta/texteditor.lua @@ -60,7 +60,7 @@ function MultiTextCursor:insertText(text) end ---@class Suggestion local Suggestion = {} ----@class SuggestionParams +---@class SuggestionParams ---@field text string The text of the suggestion. ---@field position Position The cursor position where the suggestion should be inserted. ---@field range Range The range of the text preceding the suggestion. @@ -107,15 +107,20 @@ function TextEditor:document() end ---@return MultiTextCursor cursor The cursor of the editor. function TextEditor:cursor() end ----Adds a floating widget at the specified position in the text editor. ----The widget will be positioned at the location corresponding to the given position in the ----text document and will be automatically managed to stay pined to that position. +---@class EmbeddedWidget +local EmbeddedWidget = {} + +---Closes the floating widget. +function EmbeddedWidget:close() end + +---Resizes the floating widget according to its layout. +function EmbeddedWidget:resize() end + +---Embeds a widget at the specified cursor position in the text editor. ---@param widget Widget|Layout The widget to be added as a floating widget. ---@param position integer The position in the document where the widget should appear. ----@param margins integer[] Four integers, representing left, top, right, bottom margins ----@param xPos integer Sets widget to fixed x position if x != -1, otherwise automatic x position calculation is done ----@param fillWidth boolean If true, the widget will fill remaining space from its x position to size of the TextEditor viewport -function TextEditor:addFloatingWidget(widget, position, xPos, margins, fillWidth) end +---@return EmbeddedWidget interface An interface to control the floating widget. +function TextEditor:addEmbeddedWidget(widget, position) end ---Checks if the current suggestion is locked. The suggestion is locked when the user can use it. ---@return boolean True if the suggestion is locked, false otherwise. @@ -129,5 +134,4 @@ function TextEditor:insertText(text) end ---@return TextEditor|nil editor The currently active editor or nil if there is none. function textEditor.currentEditor() end - return textEditor diff --git a/src/plugins/texteditor/textdocumentlayout.cpp b/src/plugins/texteditor/textdocumentlayout.cpp index 8a057cc3c16..53f20f0a0c8 100644 --- a/src/plugins/texteditor/textdocumentlayout.cpp +++ b/src/plugins/texteditor/textdocumentlayout.cpp @@ -777,9 +777,14 @@ QRectF TextDocumentLayout::blockBoundingRect(const QTextBlock &block) const boundingRect.setHeight(TextEditorSettings::fontSettings().lineSpacing()); } - if (TextBlockUserData *userData = textUserData(block)) - boundingRect.adjust( - 0, 0, 0, userData->additionalAnnotationHeight() + userData->additionalLineHeight()); + if (TextBlockUserData *userData = textUserData(block)) { + int additionalHeight = 0; + for (const QPointer &wdgt : userData->embeddedWidgets()) { + if (wdgt && wdgt->isVisible()) + additionalHeight += wdgt->height(); + } + boundingRect.adjust(0, 0, 0, userData->additionalAnnotationHeight() + additionalHeight); + } return boundingRect; } diff --git a/src/plugins/texteditor/textdocumentlayout.h b/src/plugins/texteditor/textdocumentlayout.h index 2974da83bc3..3c5640c26a5 100644 --- a/src/plugins/texteditor/textdocumentlayout.h +++ b/src/plugins/texteditor/textdocumentlayout.h @@ -119,9 +119,9 @@ public: { m_additionalAnnotationHeight = annotationHeight; } inline int additionalAnnotationHeight() const { return m_additionalAnnotationHeight; } - inline void setAdditionalLineHeight(int additionalLineHeight) - { m_additionalLineHeight = additionalLineHeight; } - inline int additionalLineHeight() const { return m_additionalLineHeight; } + inline void addEmbeddedWidget(QWidget *widget) { m_embeddedWidgets.append(widget); } + inline void removeEmbeddedWidget(QWidget *widget) { m_embeddedWidgets.removeAll(widget); } + inline QList> embeddedWidgets() const { return m_embeddedWidgets; } CodeFormatterData *codeFormatterData() const { return m_codeFormatterData; } void setCodeFormatterData(CodeFormatterData *data); @@ -148,13 +148,13 @@ private: uint m_foldingStartIncluded : 1; uint m_foldingEndIncluded : 1; int m_additionalAnnotationHeight = 0; - int m_additionalLineHeight = 0; Parentheses m_parentheses; CodeFormatterData *m_codeFormatterData; KSyntaxHighlighting::State m_syntaxState; QByteArray m_expectedRawStringSuffix; // A bit C++-specific, but let's be pragmatic. std::unique_ptr m_replacement; std::unique_ptr m_suggestion; + QList> m_embeddedWidgets; quint8 m_attrState = 0; }; diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index c5fc1ea5a1d..81f0bbcf084 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -749,6 +749,8 @@ public: void openTypeUnderCursor(bool openInNextSplit); qreal charWidth() const; + std::unique_ptr insertWidget(QWidget *widget, int line); + // actions void registerActions(); void updateActions(); @@ -3919,6 +3921,131 @@ qreal TextEditorWidgetPrivate::charWidth() const return QFontMetricsF(q->font()).horizontalAdvance(QLatin1Char('x')); } +class CarrierWidget : public QWidget +{ + Q_OBJECT +public: + CarrierWidget(TextEditorWidget *textEditorWidget, QWidget *embed) + : QWidget(textEditorWidget->viewport()) + , m_embed(embed) + , m_textEditorWidget(textEditorWidget) + { + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_embed); + + setFixedWidth(m_textEditorWidget->width() - m_textEditorWidget->extraAreaWidth()); + setFixedHeight(m_embed->minimumSizeHint().height()); + + connect(m_textEditorWidget, &TextEditorWidget::resized, this, [this] { + setFixedWidth(m_textEditorWidget->width() - m_textEditorWidget->extraAreaWidth()); + }); + } + + int embedHeight() { return m_embed->minimumSizeHint().height(); } + +private: + QWidget *m_embed; + TextEditorWidget *m_textEditorWidget; +}; + +EmbeddedWidgetInterface::~EmbeddedWidgetInterface() +{ + close(); +} + +void EmbeddedWidgetInterface::resize() +{ + emit resized(); +} + +void EmbeddedWidgetInterface::close() +{ + emit closed(); +} + +std::unique_ptr TextEditorWidgetPrivate::insertWidget( + QWidget *widget, int line) +{ + QPointer carrier = new CarrierWidget(q, widget); + std::unique_ptr result(new EmbeddedWidgetInterface()); + + struct State + { + int height = 0; + QTextCursor cursor; + QTextBlock block; + }; + + std::shared_ptr pState = std::make_shared(); + pState->cursor = QTextCursor(q->document()); + pState->cursor.setPosition(line); + pState->cursor.movePosition(QTextCursor::StartOfBlock); + + auto position = [this, pState, carrier] { + QTextBlock block = pState->cursor.block(); + QTC_ASSERT(block.isValid(), return); + + TextBlockUserData *userData = TextDocumentLayout::userData(block); + if (block != pState->block) { + TextBlockUserData *previousUserData = TextDocumentLayout::userData(pState->block); + if (previousUserData && userData != previousUserData) { + // We have swapped into a different block, remove it from the previous block + previousUserData->removeEmbeddedWidget(carrier); + } + userData->addEmbeddedWidget(carrier); + pState->block = block; + pState->height = 0; + } + + QRectF r = cursorBlockRect(m_document->document(), block, block.position()); + + int y = 0; + for (const auto &wdgt : userData->embeddedWidgets()) { + if (wdgt == carrier) + break; + y += wdgt->height(); + } + + QPoint pos = r.topLeft().toPoint() + + QPoint(0, TextEditorSettings::fontSettings().lineSpacing() + y); + + int h = carrier->embedHeight(); + if (h == pState->height && pos == carrier->pos()) + return; + + carrier->move(pos); + carrier->setFixedHeight(h); + + pState->height = h; + + qobject_cast(q->document()->documentLayout())->scheduleUpdate(); + }; + + position(); + + connect(widget, &QWidget::destroyed, this, [pState, carrier, this] { + if (carrier) + carrier->deleteLater(); + if (!q->document()) + return; + QTextBlock block = pState->cursor.block(); + auto userData = TextDocumentLayout::userData(block); + userData->removeEmbeddedWidget(carrier); + }); + connect(q->document()->documentLayout(), &QAbstractTextDocumentLayout::update, carrier, position); + connect(result.get(), &EmbeddedWidgetInterface::resized, carrier, position); + connect(result.get(), &EmbeddedWidgetInterface::closed, this, [this, carrier] { + if (carrier) + carrier->deleteLater(); + QAbstractTextDocumentLayout *layout = q->document()->documentLayout(); + QTimer::singleShot(0, layout, [layout] { layout->update(); }); + }); + + carrier->show(); + return result; +} + void TextEditorWidgetPrivate::registerActions() { using namespace Core::Constants; @@ -4696,6 +4823,7 @@ void TextEditorWidget::resizeEvent(QResizeEvent *e) extraAreaWidth(), cr.height() - 2 * frameWidth()))); d->adjustScrollBarRanges(); d->updateCurrentLineInScrollbar(); + emit resized(); } QRect TextEditorWidgetPrivate::foldBox() @@ -5459,7 +5587,10 @@ QRectF TextEditorWidgetPrivate::cursorBlockRect(const QTextDocument *doc, { const qreal space = charWidth(); int relativePos = cursorPosition - block.position(); + qobject_cast(m_document->document()->documentLayout()) + ->ensureBlockLayout(block); QTextLine line = block.layout()->lineForTextPosition(relativePos); + QTC_ASSERT(line.isValid(), return {}); qreal x = line.cursorToX(relativePos); qreal w = 0; if (relativePos < line.textLength() - line.textStart()) { @@ -7100,6 +7231,11 @@ TextEditorWidget::SuggestionBlocker TextEditorWidget::blockSuggestions() return d->m_suggestionBlocker; } +std::unique_ptr TextEditorWidget::insertWidget(QWidget *widget, int line) +{ + return d->insertWidget(widget, line); +} + QList TextEditorWidget::autoCompleteHighlightPositions() const { return d->m_autoCompleteHighlightPos; diff --git a/src/plugins/texteditor/texteditor.h b/src/plugins/texteditor/texteditor.h index ad32e42ae09..d98598e4661 100644 --- a/src/plugins/texteditor/texteditor.h +++ b/src/plugins/texteditor/texteditor.h @@ -102,6 +102,19 @@ enum Mask { }; } // namespace OptionalActions +class TEXTEDITOR_EXPORT EmbeddedWidgetInterface : public QObject +{ + Q_OBJECT +public: + ~EmbeddedWidgetInterface() override; + void resize(); + void close(); + +signals: + void resized(); + void closed(); +}; + class TEXTEDITOR_EXPORT BaseTextEditor : public Core::IEditor { Q_OBJECT @@ -520,6 +533,8 @@ public: // Returns an object that blocks suggestions until it is destroyed. SuggestionBlocker blockSuggestions(); + std::unique_ptr insertWidget(QWidget *widget, int line); + QList autoCompleteHighlightPositions() const; #ifdef WITH_TESTS @@ -545,6 +560,8 @@ signals: void addSavedStateToNavigationHistory(); void addCurrentStateToNavigationHistory(); + void resized(); + protected: QTextBlock blockForVisibleRow(int row) const; QTextBlock blockForVerticalOffset(int offset) const;