[education/rkward] rkward/windows: Split code navigation widget code into separate files
Thomas Friedrichsmeier
null at kde.org
Mon May 26 15:06:38 BST 2025
Git commit 562f48c7990a2186b80ac86db25315f9dba8039f by Thomas Friedrichsmeier.
Committed on 26/05/2025 at 14:06.
Pushed by tfry into branch 'master'.
Split code navigation widget code into separate files
M +1 -0 rkward/windows/CMakeLists.txt
A +278 -0 rkward/windows/rkcodenavigation.cpp [License: GPL(v2.0+)]
A +18 -0 rkward/windows/rkcodenavigation.h [License: GPL(v2.0+)]
M +2 -244 rkward/windows/rkcommandeditorwindow.cpp
https://invent.kde.org/education/rkward/-/commit/562f48c7990a2186b80ac86db25315f9dba8039f
diff --git a/rkward/windows/CMakeLists.txt b/rkward/windows/CMakeLists.txt
index 3626b3a1e..38cd2d80d 100644
--- a/rkward/windows/CMakeLists.txt
+++ b/rkward/windows/CMakeLists.txt
@@ -28,6 +28,7 @@ SET(windows_STAT_SRCS
rkdebugmessagewindow.cpp
katepluginintegration.cpp
rkcodecompletion.cpp
+ rkcodenavigation.cpp
rktexthints.cpp
)
diff --git a/rkward/windows/rkcodenavigation.cpp b/rkward/windows/rkcodenavigation.cpp
new file mode 100644
index 000000000..5379db689
--- /dev/null
+++ b/rkward/windows/rkcodenavigation.cpp
@@ -0,0 +1,278 @@
+/*
+rkcodenavigation - This file is part of the RKWard project. Created: Fri May 23 2025
+SPDX-FileCopyrightText: 2025 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
+SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "rkcodenavigation.h"
+
+#include <QApplication>
+#include <QFrame>
+#include <QKeyEvent>
+#include <QLabel>
+#include <QPainter>
+#include <QVBoxLayout>
+
+#include <KColorScheme>
+#include <KLocalizedString>
+#include <KSqueezedTextLabel>
+#include <KTextEditor/Document>
+
+#include "../misc/rkparsedscript.h"
+#include "../misc/rkstyle.h"
+#include "rkworkplace.h"
+
+#include "../debug.h"
+
+class RKCodeNavigationWidget : public QFrame {
+ public:
+ RKCodeNavigationWidget(KTextEditor::View *view, QWidget *parent) : QFrame(parent, Qt::Popup | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint), view(view), doc(view->document()) {
+ RK_TRACE(APP);
+
+ auto scheme = RKStyle::viewScheme();
+ auto pal = palette();
+ pal.setColor(backgroundRole(), scheme->background(KColorScheme::PositiveBackground).color());
+ setPalette(pal);
+
+ view->installEventFilter(this);
+ if (view->window()) view->window()->installEventFilter(this);
+ connect(doc, &KTextEditor::Document::textChanged, this, &RKCodeNavigationWidget::deleteLater);
+
+ auto box = new QVBoxLayout(this);
+ auto label = new QLabel(i18n("<b>Code Navigation</b> (<a href=\"rkward://page/rkward_code_navigation\">Help</a>)"));
+ QObject::connect(label, &QLabel::linkActivated, RKWorkplace::mainWorkplace(), &RKWorkplace::openAnyUrlString);
+ box->addWidget(label);
+
+ input = new KSqueezedTextLabel();
+ input->setFocusPolicy(Qt::StrongFocus);
+ input->installEventFilter(this);
+ box->addWidget(input);
+
+ message = new QLabel();
+ message->setTextFormat(Qt::RichText);
+ message->hide();
+ box->addWidget(message);
+
+ rmdmode = doc->highlightingMode() == u"R Markdown"_s;
+ ps = RKParsedScript(doc->text(), rmdmode);
+ StoredPosition initial;
+ // translate cursor position to string index
+ initial.pos = cursorToPosition(view->cursorPosition());
+ initial.selection = view->selectionRange();
+ stored_positions.append(initial);
+ stored_size = size();
+ updateLabel();
+ }
+
+ void updatePos() {
+ RK_TRACE(APP);
+ move(view->mapToGlobal(view->geometry().topRight() - QPoint(width() + 5, -5)));
+ }
+
+ void focusOutEvent(QFocusEvent *) override {
+ RK_TRACE(APP);
+ deleteLater();
+ }
+
+ int cursorToPosition(const KTextEditor::Cursor &cursor) {
+ RK_TRACE(APP);
+ int pos = cursor.column();
+ for (int l = 0; l < cursor.line(); ++l) {
+ pos += doc->lineLength(l) + 1;
+ }
+ return pos;
+ }
+
+ KTextEditor::Cursor positionToCursor(int pos) {
+ RK_TRACE(APP);
+ for (int l = 0; l < doc->lines(); ++l) {
+ pos -= (doc->lineLength(l) + 1);
+ if (pos < 0) {
+ return KTextEditor::Cursor(l, pos + doc->lineLength(l) + 1);
+ }
+ }
+ return KTextEditor::Cursor();
+ }
+
+ struct StoredPosition {
+ QString query;
+ int pos;
+ KTextEditor::Range selection;
+ bool hit_top;
+ bool hit_bottom;
+ QChar command;
+ };
+
+ void navigate(const StoredPosition &newpos) {
+ RK_TRACE(APP);
+ RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "navigate to %d", newpos.pos);
+ // translate final position back to cursor coordinates
+ if (!newpos.selection.isEmpty()) {
+ view->setSelection(newpos.selection);
+ view->setCursorPosition(newpos.selection.start());
+ } else {
+ if (message->isVisible()) {
+ message->hide();
+ QTimer::singleShot(0, this, [this]() {
+ resize(stored_size);
+ updatePos();
+ });
+ }
+ view->setCursorPosition(positionToCursor(newpos.pos));
+ view->setSelection(KTextEditor::Range(-1, -1, -1, -1));
+ }
+
+ updateLabel();
+ }
+
+ void handleCommand(const QChar command) {
+ RK_TRACE(APP);
+ // start from last known position
+ auto last = stored_positions.last();
+ auto pos = last.pos;
+
+ // apply navigation command
+ StoredPosition newpos;
+ newpos.pos = pos;
+ newpos.query = stored_positions.last().query + command;
+ newpos.hit_bottom = false;
+ newpos.hit_top = false;
+ newpos.command = command;
+
+ auto ci = ps.contextAtPos(pos);
+ if (command == u'n') {
+ newpos.pos = ps.getContext(ps.nextStatement(ci)).start;
+ } else if (command == u'N') {
+ newpos.pos = ps.getContext(ps.prevStatement(ci)).start;
+ } else if (command == u'i') {
+ newpos.pos = ps.getContext(ps.nextStatementOrInner(ci)).start;
+ } else if (command == u'I') {
+ newpos.pos = ps.getContext(ps.prevStatementOrInner(ci)).start;
+ } else if (command == u'o') {
+ newpos.pos = ps.getContext(ps.nextOuter(ci)).start;
+ } else if (command == u'O') {
+ newpos.pos = ps.getContext(ps.prevOuter(ci)).start;
+ } else if (command == u't') {
+ newpos.pos = ps.getContext(ps.nextToplevel(ci)).start;
+ } else if (command == u'T') {
+ newpos.pos = ps.getContext(ps.prevToplevel(ci)).start;
+ } else if (command == u'c') {
+ newpos.pos = ps.getContext(ps.nextCodeChunk(ci)).start;
+ } else if (command == u'C') {
+ newpos.pos = ps.getContext(ps.prevCodeChunk(ci)).start;
+ } else if (command == u'1') {
+ newpos.pos = 0;
+ } else if (command == u'!') {
+ newpos.pos = cursorToPosition(KTextEditor::Cursor(doc->lines() - 1, doc->lineLength(doc->lines() - 1)));
+ } else if (command == u's') {
+ auto posa = ps.getContext(ps.firstContextInStatement(ci)).start;
+ auto posb = ps.lastPositionInStatement(ci);
+ newpos.selection = KTextEditor::Range(positionToCursor(posa), positionToCursor(posb + 1));
+ } else if (command == u'S') {
+ if (!rmdmode) {
+ message->setText(i18n("Command 'S' is for R Markdown, only"));
+ message->show();
+ updatePos();
+ return;
+ }
+ auto posa = ps.getContext(ps.firstContextInChunk(ci)).start;
+ auto posb = ps.lastPositionInChunk(ci);
+ newpos.selection = KTextEditor::Range(positionToCursor(posa), positionToCursor(posb + 1));
+ } else {
+ RK_DEBUG(COMMANDEDITOR, DL_WARNING, "unknown navigation commmand");
+ message->setText(i18n("Unknown command <tt><b>%1</b></tt>").arg(command));
+ message->show();
+ updatePos();
+ return;
+ }
+
+ if (newpos.pos < 0 && command.toLower() != u's') {
+ if (rmdmode && newpos.command.toLower() != u'c') {
+ if (command.toLower() == command) {
+ message->setText(i18nc("Keep this short", "Search hit bottom.<br/><tt><b>c</b></tt> to move to next chunk."));
+ } else {
+ message->setText(i18nc("Keep this short", "Search hit top.<br/><tt><b>C</b></tt> to move to previous chunk."));
+ }
+ } else {
+ if (command.toLower() == command) {
+ message->setText(i18nc("Keep this short", "Search hit bottom.<br/><tt><b>1</b></tt> to move to top."));
+ } else {
+ message->setText(i18nc("Keep this short", "Search hit top.<br/><tt><b>!</b></tt> to move to bottom."));
+ }
+ }
+ message->show();
+ updatePos();
+ return;
+ }
+
+ stored_positions.append(newpos);
+ navigate(newpos);
+ }
+
+ bool eventFilter(QObject *from, QEvent *event) override {
+ RK_TRACE(APP);
+ if (from == view && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) {
+ updatePos();
+ } else if (from == input && event->type() == QEvent::KeyPress) {
+ auto ke = static_cast<QKeyEvent *>(event);
+ const auto text = ke->text();
+ if (ke->modifiers() == Qt::NoModifier && ke->key() == Qt::Key_Backspace && stored_positions.size() > 1) {
+ stored_positions.pop_back();
+ navigate(stored_positions.last());
+ } else if (ke->modifiers() == Qt::NoModifier && (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return)) {
+ deleteLater();
+ } else if (ke->modifiers() == Qt::NoModifier && (ke->key() == Qt::Key_Escape)) {
+ RK_ASSERT(!stored_positions.isEmpty());
+ navigate(stored_positions.first());
+ deleteLater();
+ } else if (!text.simplified().isEmpty()) {
+ handleCommand(text.back());
+ } else {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ void updateLabel() {
+ RK_TRACE(APP);
+ if (stored_positions.size() < 2) {
+ input->setText(i18n("Enter command"));
+ } else {
+ QString sequence;
+ for (const auto &p : std::as_const(stored_positions)) {
+ sequence += p.command;
+ }
+ input->setText(sequence);
+ }
+ }
+
+ void paintEvent(QPaintEvent *e) override {
+ RK_TRACE(APP);
+ QFrame::paintEvent(e);
+
+ QPainter paint(this);
+ paint.setPen(QApplication::palette().color(QPalette::Highlight));
+ paint.drawRect(0, 0, width() - 1, height() - 1);
+ }
+
+ KTextEditor::View *view;
+ KTextEditor::Document *doc;
+ KSqueezedTextLabel *input;
+ QList<StoredPosition> stored_positions;
+ RKParsedScript ps;
+ QLabel *message;
+ QSize stored_size;
+ bool rmdmode;
+};
+
+namespace RKCodeNavigation {
+ void doNavigation(KTextEditor::View *view, QWidget *parent) {
+ auto w = new RKCodeNavigationWidget(view, parent);
+ w->show();
+ w->updatePos();
+ w->input->setFocus();
+ }
+};
diff --git a/rkward/windows/rkcodenavigation.h b/rkward/windows/rkcodenavigation.h
new file mode 100644
index 000000000..3b93238c9
--- /dev/null
+++ b/rkward/windows/rkcodenavigation.h
@@ -0,0 +1,18 @@
+/*
+rkcodenavigation - This file is part of the RKWard project. Created: Fri May 23 2025
+SPDX-FileCopyrightText: 2025 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
+SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef RKCODENAVIGATION_H
+#define RKCODENAVIGATION_H
+
+#include <KTextEditor/View>
+
+namespace RKCodeNavigation {
+ void doNavigation(KTextEditor::View *view, QWidget *parent);
+ void addMenuEntries(QMenu *menu);
+};
+
+#endif
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index a3dc73bcd..e7e12f55f 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -22,9 +22,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <QFrame>
#include <QHBoxLayout>
#include <QKeyEvent>
-#include <QLineEdit>
#include <QMenu>
-#include <QPainter>
#include <QSplitter>
#include <QTemporaryDir>
#include <QTemporaryFile>
@@ -34,7 +32,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <KIO/DeleteJob>
#include <KIO/FileCopyJob>
#include <KLocalizedString>
-#include <KSqueezedTextLabel>
#include <kactioncollection.h>
#include <kactionmenu.h>
#include <kconfiggroup.h>
@@ -47,7 +44,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "../core/robjectlist.h"
#include "../misc/rkcommonfunctions.h"
#include "../misc/rkjobsequence.h"
-#include "../misc/rkparsedscript.h"
#include "../misc/rkstandardactions.h"
#include "../misc/rkstandardicons.h"
#include "../misc/rkstyle.h"
@@ -61,251 +57,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "../settings/rksettingsmodulecommandeditor.h"
#include "katepluginintegration.h"
#include "rkcodecompletion.h"
+#include "rkcodenavigation.h"
#include "rkhelpsearchwindow.h"
#include "rkhtmlwindow.h"
#include "rktexthints.h"
#include "rkworkplace.h"
-class RKCodeNavigation : public QFrame {
- private:
- RKCodeNavigation(KTextEditor::View *view, QWidget *parent) : QFrame(parent, Qt::Popup | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint), view(view), doc(view->document()) {
- auto scheme = RKStyle::viewScheme();
- auto pal = palette();
- pal.setColor(backgroundRole(), scheme->background(KColorScheme::PositiveBackground).color());
- setPalette(pal);
-
- view->installEventFilter(this);
- if (view->window()) view->window()->installEventFilter(this);
- connect(doc, &KTextEditor::Document::textChanged, this, &RKCodeNavigation::deleteLater);
-
- auto box = new QVBoxLayout(this);
- auto label = new QLabel(i18n("<b>Code Navigation</b> (<a href=\"rkward://page/rkward_code_navigation\">Help</a>)"));
- QObject::connect(label, &QLabel::linkActivated, RKWorkplace::mainWorkplace(), &RKWorkplace::openAnyUrlString);
- box->addWidget(label);
-
- input = new KSqueezedTextLabel();
- input->setFocusPolicy(Qt::StrongFocus);
- input->installEventFilter(this);
- box->addWidget(input);
-
- message = new QLabel();
- message->setTextFormat(Qt::RichText);
- message->hide();
- box->addWidget(message);
-
- rmdmode = doc->highlightingMode() == u"R Markdown"_s;
- ps = RKParsedScript(doc->text(), rmdmode);
- StoredPosition initial;
- // translate cursor position to string index
- initial.pos = cursorToPosition(view->cursorPosition());
- initial.selection = view->selectionRange();
- stored_positions.append(initial);
- stored_size = size();
- updateLabel();
- }
-
- void updatePos() {
- move(view->mapToGlobal(view->geometry().topRight() - QPoint(width() + 5, -5)));
- }
-
- void focusOutEvent(QFocusEvent *) override {
- deleteLater();
- }
-
- int cursorToPosition(const KTextEditor::Cursor &cursor) {
- int pos = cursor.column();
- for (int l = 0; l < cursor.line(); ++l) {
- pos += doc->lineLength(l) + 1;
- }
- return pos;
- }
-
- KTextEditor::Cursor positionToCursor(int pos) {
- for (int l = 0; l < doc->lines(); ++l) {
- pos -= (doc->lineLength(l) + 1);
- if (pos < 0) {
- return KTextEditor::Cursor(l, pos + doc->lineLength(l) + 1);
- }
- }
- return KTextEditor::Cursor();
- }
-
- struct StoredPosition {
- QString query;
- int pos;
- KTextEditor::Range selection;
- bool hit_top;
- bool hit_bottom;
- QChar command;
- };
-
- void navigate(const StoredPosition &newpos) {
- RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "navigate to %d", newpos.pos);
- // translate final position back to cursor coordinates
- if (!newpos.selection.isEmpty()) {
- view->setSelection(newpos.selection);
- view->setCursorPosition(newpos.selection.start());
- } else {
- if (message->isVisible()) {
- message->hide();
- QTimer::singleShot(0, this, [this]() {
- resize(stored_size);
- updatePos();
- });
- }
- view->setCursorPosition(positionToCursor(newpos.pos));
- view->setSelection(KTextEditor::Range(-1, -1, -1, -1));
- }
-
- updateLabel();
- }
-
- void handleCommand(const QChar command) {
- // start from last known position
- auto last = stored_positions.last();
- auto pos = last.pos;
-
- // apply navigation command
- StoredPosition newpos;
- newpos.pos = pos;
- newpos.query = stored_positions.last().query + command;
- newpos.hit_bottom = false;
- newpos.hit_top = false;
- newpos.command = command;
-
- auto ci = ps.contextAtPos(pos);
- if (command == u'n') {
- newpos.pos = ps.getContext(ps.nextStatement(ci)).start;
- } else if (command == u'N') {
- newpos.pos = ps.getContext(ps.prevStatement(ci)).start;
- } else if (command == u'i') {
- newpos.pos = ps.getContext(ps.nextStatementOrInner(ci)).start;
- } else if (command == u'I') {
- newpos.pos = ps.getContext(ps.prevStatementOrInner(ci)).start;
- } else if (command == u'o') {
- newpos.pos = ps.getContext(ps.nextOuter(ci)).start;
- } else if (command == u'O') {
- newpos.pos = ps.getContext(ps.prevOuter(ci)).start;
- } else if (command == u't') {
- newpos.pos = ps.getContext(ps.nextToplevel(ci)).start;
- } else if (command == u'T') {
- newpos.pos = ps.getContext(ps.prevToplevel(ci)).start;
- } else if (command == u'c') {
- newpos.pos = ps.getContext(ps.nextCodeChunk(ci)).start;
- } else if (command == u'C') {
- newpos.pos = ps.getContext(ps.prevCodeChunk(ci)).start;
- } else if (command == u'1') {
- newpos.pos = 0;
- } else if (command == u'!') {
- newpos.pos = cursorToPosition(KTextEditor::Cursor(doc->lines() - 1, doc->lineLength(doc->lines() - 1)));
- } else if (command == u's') {
- auto posa = ps.getContext(ps.firstContextInStatement(ci)).start;
- auto posb = ps.lastPositionInStatement(ci);
- newpos.selection = KTextEditor::Range(positionToCursor(posa), positionToCursor(posb + 1));
- } else if (command == u'S') {
- if (!rmdmode) {
- message->setText(i18n("Command 'S' is for R Markdown, only"));
- message->show();
- updatePos();
- return;
- }
- auto posa = ps.getContext(ps.firstContextInChunk(ci)).start;
- auto posb = ps.lastPositionInChunk(ci);
- newpos.selection = KTextEditor::Range(positionToCursor(posa), positionToCursor(posb + 1));
- } else {
- RK_DEBUG(COMMANDEDITOR, DL_WARNING, "unknown navigation commmand");
- message->setText(i18n("Unknown command <tt><b>%1</b></tt>").arg(command));
- message->show();
- updatePos();
- return;
- }
-
- if (newpos.pos < 0 && command.toLower() != u's') {
- if (rmdmode && newpos.command.toLower() != u'c') {
- if (command.toLower() == command) {
- message->setText(i18nc("Keep this short", "Search hit bottom.<br/><tt><b>c</b></tt> to move to next chunk."));
- } else {
- message->setText(i18nc("Keep this short", "Search hit top.<br/><tt><b>C</b></tt> to move to previous chunk."));
- }
- } else {
- if (command.toLower() == command) {
- message->setText(i18nc("Keep this short", "Search hit bottom.<br/><tt><b>1</b></tt> to move to top."));
- } else {
- message->setText(i18nc("Keep this short", "Search hit top.<br/><tt><b>!</b></tt> to move to bottom."));
- }
- }
- message->show();
- updatePos();
- return;
- }
-
- stored_positions.append(newpos);
- navigate(newpos);
- }
-
- bool eventFilter(QObject *from, QEvent *event) override {
- if (from == view && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) {
- updatePos();
- } else if (from == input && event->type() == QEvent::KeyPress) {
- auto ke = static_cast<QKeyEvent *>(event);
- const auto text = ke->text();
- if (ke->modifiers() == Qt::NoModifier && ke->key() == Qt::Key_Backspace && stored_positions.size() > 1) {
- stored_positions.pop_back();
- navigate(stored_positions.last());
- } else if (ke->modifiers() == Qt::NoModifier && (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return)) {
- deleteLater();
- } else if (ke->modifiers() == Qt::NoModifier && (ke->key() == Qt::Key_Escape)) {
- RK_ASSERT(!stored_positions.isEmpty());
- navigate(stored_positions.first());
- deleteLater();
- } else if (!text.simplified().isEmpty()) {
- handleCommand(text.back());
- } else {
- return false;
- }
- return true;
- }
- return false;
- }
-
- void updateLabel() {
- if (stored_positions.size() < 2) {
- input->setText(i18n("Enter command"));
- } else {
- QString sequence;
- for (const auto &p : std::as_const(stored_positions)) {
- sequence += p.command;
- }
- input->setText(sequence);
- }
- }
-
- void paintEvent(QPaintEvent *e) override {
- QFrame::paintEvent(e);
-
- QPainter paint(this);
- paint.setPen(QApplication::palette().color(QPalette::Highlight));
- paint.drawRect(0, 0, width() - 1, height() - 1);
- }
-
- public:
- static void doNavigation(KTextEditor::View *view, QWidget *parent) {
- auto w = new RKCodeNavigation(view, parent);
- w->show();
- w->updatePos();
- w->input->setFocus();
- }
-
- private:
- KTextEditor::View *view;
- KTextEditor::Document *doc;
- KSqueezedTextLabel *input;
- QList<StoredPosition> stored_positions;
- RKParsedScript ps;
- QLabel *message;
- QSize stored_size;
- bool rmdmode;
-};
+#include "../debug.h"
RKCommandEditorWindowPart::RKCommandEditorWindowPart(QWidget *parent) : KParts::Part(parent) {
RK_TRACE(COMMANDEDITOR);
More information about the rkward-tracker
mailing list