[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();
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.