[education/labplot] src/frontend: [scripting] use QTextBrowser instead of QPlainTextEdit for the terminal output and highlight errors and warnings.
Alexander Semke <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit c221f5520399f5afd5e4e061e17b56e4c91f225d by Alexander Semke.
Committed on 28/07/2026 at 14:43.
Pushed by asemke into branch 'master'.
[scripting] use QTextBrowser instead of QPlainTextEdit for the terminal output and highlight errors and warnings.
M +120 -5 src/frontend/script/ScriptEditor.cpp
M +9 -0 src/frontend/script/ScriptEditor.h
M +4 -1 src/frontend/ui/script/scripteditorwidget.ui
https://invent.kde.org/education/labplot/-/commit/c221f5520399f5afd5e4e061e17b56e4c91f225d
diff --git a/src/frontend/script/ScriptEditor.cpp b/src/frontend/script/ScriptEditor.cpp
index 655aeb0133..f96d8f7e55 100644
--- a/src/frontend/script/ScriptEditor.cpp
+++ b/src/frontend/script/ScriptEditor.cpp
@@ -27,7 +27,12 @@
#include <QVariant>
#include <QFont>
#include <QObject>
-#include <QPlainTextEdit>
+#include <QTextBrowser>
+#include <QRegularExpression>
+#include <QTextCursor>
+#include <QTextCharFormat>
+#include <QClipboard>
+#include <QApplication>
ScriptEditor::ScriptEditor(Script* script, QWidget* parent)
: QWidget(parent), m_script(script) {
@@ -54,6 +59,14 @@ ScriptEditor::ScriptEditor(Script* script, QWidget* parent)
});
ui.output->setReadOnly(true);
+ ui.output->setOpenLinks(false);
+
+ // Connect anchor click handler for line navigation
+ connect(ui.output, &QTextBrowser::anchorClicked, this, &ScriptEditor::handleAnchorClicked);
+
+ // Setup context menu for output
+ ui.output->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui.output, &QTextBrowser::customContextMenuRequested, this, &ScriptEditor::showOutputContextMenu);
}
ScriptEditor::~ScriptEditor() {
@@ -104,19 +117,33 @@ void ScriptEditor::initActions() {
m_clearOutputAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear")), QStringLiteral("Clear Output"), this);
m_clearOutputAction->setWhatsThis(QStringLiteral("Clear the output of the script editor"));
connect(m_clearOutputAction, &QAction::triggered, this, &ScriptEditor::clearOutput);
+
+ m_copySelectedAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), QStringLiteral("Copy Selected"), this);
+ m_copySelectedAction->setWhatsThis(QStringLiteral("Copy selected text from output"));
+ connect(m_copySelectedAction, &QAction::triggered, [this]() {
+ QApplication::clipboard()->setText(ui.output->textCursor().selectedText());
+ });
+
+ m_copyAllOutputAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), QStringLiteral("Copy All Output"), this);
+ m_copyAllOutputAction->setWhatsThis(QStringLiteral("Copy all output text"));
+ connect(m_copyAllOutputAction, &QAction::triggered, [this]() {
+ QApplication::clipboard()->setText(ui.output->toPlainText());
+ });
}
-void ScriptEditor::writeOutput(bool /*isErr*/, const QString& msg) {
+void ScriptEditor::writeOutput(bool isErr, const QString& msg) {
DEBUG(Q_FUNC_INFO << ", text = '" << msg.toStdString() << "'")
if (msg.isEmpty())
return;
- // avoid additional newlines by using a cursor
+ // Process the output text to add links and formatting
+ QString processedHtml = processOutputText(isErr, msg);
+
+ // Insert formatted HTML at the end
auto cursor = ui.output->textCursor();
cursor.movePosition(QTextCursor::End);
- cursor.insertText(msg);
ui.output->setTextCursor(cursor);
-
+ ui.output->insertHtml(processedHtml);
}
void ScriptEditor::setSplitterState(const QByteArray& state) {
@@ -159,3 +186,91 @@ void ScriptEditor::clearOutput() {
ui.output->setReadOnly(true);
setOutputFont(currentOutputFont);
}
+
+QString ScriptEditor::processOutputText(bool isErr, const QString& text) {
+ QString html;
+ QString escapedText = text.toHtmlEscaped();
+
+ // Detect and format Python traceback patterns
+ // Pattern 1: "File "<string>", line 5, in <module>"
+ static QRegularExpression fileLinePattern(
+ QStringLiteral(R"(File\s+"[^"]*",\s+line\s+(\d+))"),
+ QRegularExpression::CaseInsensitiveOption
+ );
+
+ // Pattern 2: Error/Exception names (e.g., "NameError:", "ValueError:", "TypeError:")
+ static QRegularExpression errorPattern(
+ QStringLiteral(R"(^(\w+Error|\w+Exception|Traceback):)"),
+ QRegularExpression::MultilineOption
+ );
+
+ // Pattern 3: Warning patterns
+ static QRegularExpression warningPattern(
+ QStringLiteral(R"(\bwarning\b|\bWARN\b)"),
+ QRegularExpression::CaseInsensitiveOption
+ );
+
+ QString processedText = escapedText;
+
+ // Add clickable links for line references
+ QRegularExpressionMatchIterator it = fileLinePattern.globalMatch(processedText);
+ int offset = 0;
+ while (it.hasNext()) {
+ QRegularExpressionMatch match = it.next();
+ QString lineNum = match.captured(1);
+ QString matchedText = match.captured(0);
+ QString link = QStringLiteral("<a href=\"line:%1\">%2</a>").arg(lineNum, matchedText);
+
+ int startPos = match.capturedStart(0) + offset;
+ int length = match.capturedLength(0);
+ processedText.replace(startPos, length, link);
+ offset += link.length() - length;
+ }
+
+ // Apply color formatting
+ if (isErr) {
+ // Error output - make it red
+ html = QStringLiteral("<span style=\"color: #d32f2f;\">%1</span>").arg(processedText);
+ } else if (warningPattern.match(processedText).hasMatch()) {
+ // Warning - make it orange
+ html = QStringLiteral("<span style=\"color: #f57c00;\">%1</span>").arg(processedText);
+ } else if (errorPattern.match(processedText).hasMatch()) {
+ // Exception names - make them red
+ html = QStringLiteral("<span style=\"color: #d32f2f;\">%1</span>").arg(processedText);
+ } else {
+ // Normal output
+ html = processedText;
+ }
+
+ return html;
+}
+
+void ScriptEditor::handleAnchorClicked(const QUrl& url) {
+ DEBUG(Q_FUNC_INFO << ", URL = " << url.toString().toStdString())
+
+ // Handle line:N links
+ if (url.scheme() == QLatin1String("line")) {
+ QString lineStr = url.path();
+ bool ok;
+ int line = lineStr.toInt(&ok);
+ if (ok && line > 0 && m_kTextEditorView) {
+ // Jump to the specified line in the editor (1-based)
+ m_kTextEditorView->setCursorPosition(KTextEditor::Cursor(line - 1, 0));
+ m_kTextEditorView->setFocus();
+ }
+ }
+}
+
+void ScriptEditor::showOutputContextMenu(const QPoint& pos) {
+ QMenu menu(this);
+
+ bool hasSelection = ui.output->textCursor().hasSelection();
+ m_copySelectedAction->setEnabled(hasSelection);
+
+ menu.addAction(m_copySelectedAction);
+ menu.addAction(m_copyAllOutputAction);
+ menu.addSeparator();
+ menu.addAction(m_clearOutputAction);
+
+ menu.exec(ui.output->mapToGlobal(pos));
+}
diff --git a/src/frontend/script/ScriptEditor.h b/src/frontend/script/ScriptEditor.h
index dadbee0f47..f3cf938cf0 100644
--- a/src/frontend/script/ScriptEditor.h
+++ b/src/frontend/script/ScriptEditor.h
@@ -37,6 +37,10 @@ public:
void registerShortcuts();
void unregisterShortcuts();
+private Q_SLOTS:
+ void handleAnchorClicked(const QUrl&);
+ void showOutputContextMenu(const QPoint&);
+
public Q_SLOTS:
void createContextMenu(QMenu*);
void run();
@@ -48,6 +52,8 @@ private:
KTextEditor::View* m_kTextEditorView{nullptr};
QAction* m_runScriptAction{nullptr};
QAction* m_clearOutputAction{nullptr};
+ QAction* m_copySelectedAction{nullptr};
+ QAction* m_copyAllOutputAction{nullptr};
void initActions();
void initMenus();
@@ -55,5 +61,8 @@ private:
QByteArray splitterState();
void setOutputFont(const QFont&);
QFont outputFont();
+
+ QString processOutputText(bool isErr, const QString& text);
+ void applyOutputFormatting(const QString& html, bool isErr);
};
#endif
diff --git a/src/frontend/ui/script/scripteditorwidget.ui b/src/frontend/ui/script/scripteditorwidget.ui
index ca5b602c7e..b714ecbb7f 100644
--- a/src/frontend/ui/script/scripteditorwidget.ui
+++ b/src/frontend/ui/script/scripteditorwidget.ui
@@ -99,13 +99,16 @@
</widget>
</item>
<item>
- <widget class="QPlainTextEdit" name="output">
+ <widget class="QTextBrowser" name="output">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
+ <property name="openLinks">
+ <bool>false</bool>
+ </property>
</widget>
</item>
</layout>