TextEditor: Add insertWidget function

Allows to embed a widget into a line that will do its best
to stay in position.

Change-Id: I72590814057eb92bd17978a63bb4f7132410212f
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Marcus Tillmanns
2024-10-30 10:11:00 +01:00
parent b70a774712
commit 6451d42ed3
9 changed files with 283 additions and 100 deletions

View File

@@ -77,6 +77,7 @@ local function setup()
script()()
end,
})
require 'tst_texteditor'.setup()
end
return { setup = setup }

View File

@@ -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,
}

View File

@@ -46,65 +46,14 @@ TextEditor::TextEditorWidget *getSuggestionReadyEditorWidget(TextEditor::TextDoc
return widget;
}
void fillRemainingViewportWidth(QWidget *widget, const QSize &viewportSize, const QMargins &margins)
std::unique_ptr<EmbeddedWidgetInterface> 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<EmbeddedWidgetInterface> 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>(
"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<Layouting::Layout *, Layouting::Widget *, QWidget *>;
static auto toWidget = [](LayoutOrWidget &arg) {
return std::visit(
[](auto &&arg) -> QWidget * {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, Layouting::Widget *>)
return arg->emerge();
else if constexpr (std::is_same_v<T, QWidget *>)
return arg;
else if constexpr (std::is_same_v<T, Layouting::Layout *>)
return arg->emerge();
else
return nullptr;
},
arg);
};
result.new_usertype<EmbeddedWidgetInterface>(
"EmbeddedWidgetInterface",
sol::no_constructor,
"resize",
&EmbeddedWidgetInterface::resize,
"close",
&EmbeddedWidgetInterface::close);
result.new_usertype<BaseTextEditor>(
"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"));

View File

@@ -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

View File

@@ -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

View File

@@ -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<QWidget> &wdgt : userData->embeddedWidgets()) {
if (wdgt && wdgt->isVisible())
additionalHeight += wdgt->height();
}
boundingRect.adjust(0, 0, 0, userData->additionalAnnotationHeight() + additionalHeight);
}
return boundingRect;
}

View File

@@ -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<QPointer<QWidget>> 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<QTextDocument> m_replacement;
std::unique_ptr<TextSuggestion> m_suggestion;
QList<QPointer<QWidget>> m_embeddedWidgets;
quint8 m_attrState = 0;
};

View File

@@ -749,6 +749,8 @@ public:
void openTypeUnderCursor(bool openInNextSplit);
qreal charWidth() const;
std::unique_ptr<EmbeddedWidgetInterface> 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<EmbeddedWidgetInterface> TextEditorWidgetPrivate::insertWidget(
QWidget *widget, int line)
{
QPointer<CarrierWidget> carrier = new CarrierWidget(q, widget);
std::unique_ptr<EmbeddedWidgetInterface> result(new EmbeddedWidgetInterface());
struct State
{
int height = 0;
QTextCursor cursor;
QTextBlock block;
};
std::shared_ptr<State> pState = std::make_shared<State>();
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<TextDocumentLayout *>(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<TextDocumentLayout *>(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<EmbeddedWidgetInterface> TextEditorWidget::insertWidget(QWidget *widget, int line)
{
return d->insertWidget(widget, line);
}
QList<QTextCursor> TextEditorWidget::autoCompleteHighlightPositions() const
{
return d->m_autoCompleteHighlightPos;

View File

@@ -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<EmbeddedWidgetInterface> insertWidget(QWidget *widget, int line);
QList<QTextCursor> 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;