[education/labplot] src/frontend/script: [scripting] implemented a custom completion popup widget.
Alexander Semke <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 7fa5a3bc98fcc5a3f6adf9a4450d15f6c461ba55 by Alexander Semke.
Committed on 01/08/2026 at 20:22.
Pushed by asemke into branch 'master'.
[scripting] implemented a custom completion popup widget.
M +8 -0 src/frontend/script/ScriptCompletionModel.cpp
M +5 -0 src/frontend/script/ScriptCompletionModel.h
M +138 -0 src/frontend/script/ScriptEditor.cpp
M +8 -0 src/frontend/script/ScriptEditor.h
https://invent.kde.org/education/labplot/-/commit/7fa5a3bc98fcc5a3f6adf9a4450d15f6c461ba55
diff --git a/src/frontend/script/ScriptCompletionModel.cpp b/src/frontend/script/ScriptCompletionModel.cpp
index b9eeae79ee..353c74bd4f 100644
--- a/src/frontend/script/ScriptCompletionModel.cpp
+++ b/src/frontend/script/ScriptCompletionModel.cpp
@@ -209,6 +209,9 @@ void ScriptCompletionModel::startCompletionRequest() {
setRowCount(m_matches.size());
endResetModel();
+ // Emit signal for custom popup
+ Q_EMIT modelIsReady(m_matches);
+
// DEBUG(Q_FUNC_INFO << ", Showing " << m_matches.size() << " completions for prefix '" << prefix << "'")
}
@@ -283,3 +286,8 @@ bool ScriptCompletionModel::shouldStartCompletion(KTextEditor::View* view, const
return false;
}
+
+void ScriptCompletionModel::abortCompletion() {
+ if (m_debounceTimer && m_debounceTimer->isActive())
+ m_debounceTimer->stop();
+}
diff --git a/src/frontend/script/ScriptCompletionModel.h b/src/frontend/script/ScriptCompletionModel.h
index 33c2b6928b..eb77b7a469 100644
--- a/src/frontend/script/ScriptCompletionModel.h
+++ b/src/frontend/script/ScriptCompletionModel.h
@@ -40,8 +40,13 @@ public:
KTextEditor::Range completionRange(KTextEditor::View*, const KTextEditor::Cursor&) override;
bool shouldStartCompletion(KTextEditor::View*, const QString&, bool, const KTextEditor::Cursor&) override;
+ void abortCompletion();
bool initPylabplotSymbols();
+ const QList<CompletionItem>& matches() const { return m_matches; }
+
+Q_SIGNALS:
+ void modelIsReady(const QList<CompletionItem>&);
private Q_SLOTS:
void startCompletionRequest();
diff --git a/src/frontend/script/ScriptEditor.cpp b/src/frontend/script/ScriptEditor.cpp
index 98d8faed55..19f869e2c9 100644
--- a/src/frontend/script/ScriptEditor.cpp
+++ b/src/frontend/script/ScriptEditor.cpp
@@ -35,6 +35,8 @@
#include <QTextCharFormat>
#include <QClipboard>
#include <QApplication>
+#include <QListWidget>
+#include <QKeyEvent>
ScriptEditor::ScriptEditor(Script* script, QWidget* parent)
: QWidget(parent), m_script(script) {
@@ -74,6 +76,9 @@ ScriptEditor::ScriptEditor(Script* script, QWidget* parent)
m_completionModel = new ScriptCompletionModel(this);
m_kTextEditorView->registerCompletionModel(m_completionModel);
m_kTextEditorView->setAutomaticInvocationEnabled(true);
+
+ // Connect to show custom popup (no parameter needed - we'll query the model)
+ connect(m_completionModel, &ScriptCompletionModel::modelIsReady, this, &ScriptEditor::showCustomCompleter);
}
ScriptEditor::~ScriptEditor() {
@@ -349,3 +354,136 @@ void ScriptEditor::showOutputContextMenu(const QPoint& pos) {
menu.exec(ui.output->mapToGlobal(pos));
}
+
+void ScriptEditor::showCustomCompleter() {
+ if (!m_completionModel)
+ return;
+
+ // Get matches from the completion model
+ const auto& matches = m_completionModel->matches();
+
+ // Hide completer if no matches
+ if (matches.isEmpty()) {
+ if (m_customCompleter && m_customCompleter->isVisible())
+ m_customCompleter->hide();
+ return;
+ }
+
+ // Create completer widget on first use
+ if (!m_customCompleter) {
+ m_customCompleter = new QListWidget();
+ m_customCompleter->setWindowFlags(Qt::Popup);
+ m_customCompleter->setFocusPolicy(Qt::NoFocus);
+ m_customCompleter->setFocusProxy(m_kTextEditorView);
+ m_customCompleter->installEventFilter(this);
+
+ connect(m_customCompleter, &QListWidget::itemActivated, this, &ScriptEditor::onCompleterItemSelected);
+ connect(m_customCompleter, &QListWidget::itemClicked, this, &ScriptEditor::onCompleterItemSelected);
+ }
+
+ // Populate with matches
+ m_customCompleter->clear();
+ for (const auto& item : matches) {
+ QString displayText = item.name;
+ // Add icon or indicator for type
+ if (item.isClass)
+ displayText += QStringLiteral(" [class]");
+ else if (item.isFunction)
+ displayText += QStringLiteral(" [function]");
+ else if (item.isVariable)
+ displayText += QStringLiteral(" [variable]");
+
+ auto* listItem = new QListWidgetItem(displayText, m_customCompleter);
+ listItem->setData(Qt::UserRole, item.name); // Store clean name for insertion
+ }
+ m_customCompleter->setCurrentRow(0);
+
+ // Position below cursor
+ KTextEditor::Cursor cursor = m_kTextEditorView->cursorPosition();
+ QPoint cursorPos = m_kTextEditorView->cursorPositionCoordinates();
+ QPoint globalPos = m_kTextEditorView->mapToGlobal(cursorPos);
+
+ // Move below cursor line
+ globalPos.setY(globalPos.y() + m_kTextEditorView->fontMetrics().height());
+
+ m_customCompleter->move(globalPos);
+ m_customCompleter->setMinimumWidth(200);
+ m_customCompleter->setMaximumHeight(200);
+ m_customCompleter->show();
+}
+
+void ScriptEditor::onCompleterItemSelected() {
+ if (!m_customCompleter || !m_kTextEditorView)
+ return;
+
+ QListWidgetItem* item = m_customCompleter->currentItem();
+ if (!item)
+ return;
+
+ // Get clean name from UserRole data
+ QString textToInsert = item->data(Qt::UserRole).toString();
+
+ // Check if it's a function or class by looking at the display text
+ QString displayText = item->text();
+ bool isFunction = displayText.contains(QStringLiteral("[function]"));
+ bool isClass = displayText.contains(QStringLiteral("[class]"));
+
+ // Add parentheses for functions and classes
+ if (isFunction || isClass)
+ textToInsert += QStringLiteral("()");
+
+ // Replace current word with completion
+ KTextEditor::Cursor currentPos = m_kTextEditorView->cursorPosition();
+ KTextEditor::Range wordRange = m_kTextEditorView->document()->wordRangeAt(currentPos);
+ m_kTextEditorView->document()->replaceText(wordRange, textToInsert);
+
+ // Position cursor inside parentheses for functions/classes
+ if (isFunction || isClass) {
+ KTextEditor::Cursor newCursorPos = wordRange.start();
+ newCursorPos.setColumn(newCursorPos.column() + textToInsert.length() - 1); // -1 to be inside ()
+ m_kTextEditorView->setCursorPosition(newCursorPos);
+ }
+
+ // Hide completer
+ m_customCompleter->hide();
+
+ // Return focus to editor
+ m_kTextEditorView->setFocus();
+}
+
+bool ScriptEditor::eventFilter(QObject* obj, QEvent* event) {
+ if (obj == m_customCompleter && m_customCompleter->isVisible()) {
+ if (event->type() == QEvent::KeyPress) {
+ auto* keyEvent = static_cast<QKeyEvent*>(event);
+ switch (keyEvent->key()) {
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ // Accept current selection
+ onCompleterItemSelected();
+ return true;
+
+ case Qt::Key_Escape:
+ // Hide completer
+ m_customCompleter->hide();
+ m_kTextEditorView->setFocus();
+ return true;
+
+ case Qt::Key_Up:
+ case Qt::Key_Down:
+ case Qt::Key_PageUp:
+ case Qt::Key_PageDown:
+ // Let the list widget handle navigation
+ return false;
+
+ default:
+ // Pass other keys to editor and hide completer
+ m_customCompleter->hide();
+ m_kTextEditorView->setFocus();
+ QApplication::sendEvent(m_kTextEditorView, event);
+ return true;
+ }
+ }
+ }
+
+ return QWidget::eventFilter(obj, event);
+}
diff --git a/src/frontend/script/ScriptEditor.h b/src/frontend/script/ScriptEditor.h
index f2423e67ef..eb5f19ae0d 100644
--- a/src/frontend/script/ScriptEditor.h
+++ b/src/frontend/script/ScriptEditor.h
@@ -19,7 +19,9 @@ class QMenu;
class Script;
class QToolBar;
class QToolButton;
+class QListWidget;
class ScriptCompletionModel;
+
namespace KTextEditor{
class View;
}
@@ -39,9 +41,14 @@ public:
void registerShortcuts();
void unregisterShortcuts();
+protected:
+ bool eventFilter(QObject* obj, QEvent* event) override;
+
private Q_SLOTS:
void handleAnchorClicked(const QUrl&);
void showOutputContextMenu(const QPoint&);
+ void showCustomCompleter();
+ void onCompleterItemSelected();
public Q_SLOTS:
void createContextMenu(QMenu*);
@@ -57,6 +64,7 @@ private:
QAction* m_copySelectedAction{nullptr};
QAction* m_copyAllOutputAction{nullptr};
ScriptCompletionModel* m_completionModel{nullptr};
+ QListWidget* m_customCompleter{nullptr};
void initActions();
void initMenus();