[education/rkward] rkward/windows: Restructure implementation of code navigation context menu
Thomas Friedrichsmeier
null at kde.org
Mon May 26 15:06:38 BST 2025
Git commit eb873518bcfabaa5efa68a96c8dab6e68cbb93d3 by Thomas Friedrichsmeier.
Committed on 26/05/2025 at 14:06.
Pushed by tfry into branch 'master'.
Restructure implementation of code navigation context menu
Importantly, menu entries can now be give a custom shortcut, but also
they do not imply a parse, unless the document text has changed.
M +102 -77 rkward/windows/rkcodenavigation.cpp
M +16 -5 rkward/windows/rkcodenavigation.h
M +3 -4 rkward/windows/rkcommandeditorwindow.cpp
M +4 -4 rkward/windows/rkcommandeditorwindowpart.rc
https://invent.kde.org/education/rkward/-/commit/eb873518bcfabaa5efa68a96c8dab6e68cbb93d3
diff --git a/rkward/windows/rkcodenavigation.cpp b/rkward/windows/rkcodenavigation.cpp
index 639503e93..dd7df63db 100644
--- a/rkward/windows/rkcodenavigation.cpp
+++ b/rkward/windows/rkcodenavigation.cpp
@@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "rkcodenavigation.h"
+#include <QActionGroup>
#include <QApplication>
#include <QFrame>
#include <QKeyEvent>
@@ -19,6 +20,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <KLocalizedString>
#include <KSqueezedTextLabel>
#include <KTextEditor/Document>
+#include <KTextEditor/Message>
#include "../misc/rkparsedscript.h"
#include "../misc/rkstyle.h"
@@ -26,9 +28,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "../debug.h"
-class RKCodeNavigationBase {
+/** TODO: This would benefit from some re-organisation: Wrapper around parsed script and helper functions that may be used
+ * with or without ann RKCodeNavigationWidget. Probably, this should own the widget, rather than the other way around. */
+class RKCodeNavigationInternal : public QObject {
public:
- RKCodeNavigationBase(KTextEditor::View *view) :
+ RKCodeNavigationInternal(KTextEditor::View *view, QObject *parent) :
+ QObject(parent),
view(view),
doc(view->document()),
rmdmode(doc->highlightingMode() == u"R Markdown"_s),
@@ -50,7 +55,7 @@ class RKCodeNavigationBase {
return pos;
}
- StoredPosition handleCommandBase(const QChar &command) const {
+ StoredPosition handleCommand(const QChar &command) const {
auto last = viewPosition();
auto pos = last.pos;
@@ -121,7 +126,7 @@ class RKCodeNavigationBase {
return newpos;
}
- void navigateBase(const StoredPosition &newpos) const {
+ void navigate(const StoredPosition &newpos) const {
RK_TRACE(APP);
RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "navigate to %d", newpos.pos);
// translate final position back to cursor coordinates
@@ -132,6 +137,14 @@ class RKCodeNavigationBase {
view->setCursorPosition(positionToCursor(newpos.pos));
view->setSelection(KTextEditor::Range(-1, -1, -1, -1));
}
+
+ if (!newpos.message.isEmpty()) {
+ auto msg = new KTextEditor::Message(newpos.message, KTextEditor::Message::Information);
+ msg->setPosition(KTextEditor::Message::BottomInView);
+ msg->setAutoHide(2000);
+ msg->setAutoHideMode(KTextEditor::Message::Immediate);
+ doc->postMessage(msg);
+ }
}
int cursorToPosition(const KTextEditor::Cursor &cursor) const {
@@ -154,16 +167,15 @@ class RKCodeNavigationBase {
return KTextEditor::Cursor();
}
- protected:
KTextEditor::View *view;
KTextEditor::Document *doc;
bool rmdmode;
RKParsedScript ps;
};
-class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
+class RKCodeNavigationWidget : public QFrame {
public:
- RKCodeNavigationWidget(KTextEditor::View *view, QWidget *parent) : QFrame(parent, Qt::Popup | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint), RKCodeNavigationBase(view) {
+ RKCodeNavigationWidget(QWidget *parent, RKCodeNavigationInternal *internal) : QFrame(parent, Qt::Popup | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint), internal(internal) {
RK_TRACE(APP);
auto scheme = RKStyle::viewScheme();
@@ -171,9 +183,9 @@ class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
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);
+ internal->view->installEventFilter(this);
+ if (internal->view->window()) internal->view->window()->installEventFilter(this);
+ connect(internal->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>)"));
@@ -191,13 +203,13 @@ class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
box->addWidget(message);
stored_size = size();
- stored_positions.append(viewPosition());
+ stored_positions.append(internal->viewPosition());
updateLabel();
}
void updatePos() {
RK_TRACE(APP);
- move(view->mapToGlobal(view->geometry().topRight() - QPoint(width() + 5, -5)));
+ move(internal->view->mapToGlobal(internal->view->geometry().topRight() - QPoint(width() + 5, -5)));
}
void focusOutEvent(QFocusEvent *) override {
@@ -205,9 +217,9 @@ class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
deleteLater();
}
- void navigate(const StoredPosition &newpos) {
+ void navigate(const RKCodeNavigationInternal::StoredPosition &newpos) {
RK_TRACE(APP);
- navigateBase(newpos);
+ internal->navigate(newpos);
if (message->isVisible()) {
message->hide();
QTimer::singleShot(0, this, [this]() {
@@ -221,8 +233,9 @@ class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
void handleCommand(const QChar command) {
RK_TRACE(APP);
- auto pos = handleCommandBase(command);
+ auto pos = internal->handleCommand(command);
if (!pos.message.isEmpty()) {
+ message->setText(pos.message);
message->show();
updatePos();
return;
@@ -234,14 +247,16 @@ class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
bool eventFilter(QObject *from, QEvent *event) override {
RK_TRACE(APP);
- if (from == view && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) {
+ if (from == internal->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());
+ if (ke->modifiers() == Qt::NoModifier && ke->key() == Qt::Key_Backspace) {
+ if (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)) {
@@ -283,66 +298,76 @@ class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
KSqueezedTextLabel *input;
QLabel *message;
QSize stored_size;
- QList<StoredPosition> stored_positions;
+ QList<RKCodeNavigationInternal::StoredPosition> stored_positions;
+ RKCodeNavigationInternal *internal;
};
-namespace RKCodeNavigation {
- void doNavigation(KTextEditor::View *view, QWidget *parent) {
- auto w = new RKCodeNavigationWidget(view, parent);
+RKCodeNavigation::RKCodeNavigation(KTextEditor::View *view, QWidget *parent) :
+ QObject(parent), view(view), internal(nullptr) {
+ RK_TRACE(APP);
+
+ connect(view->document(), &KTextEditor::Document::textChanged, this, [this]() {
+ if (internal) {
+ internal->deleteLater();
+ internal = nullptr;
+ }
+ });
+
+ QMenu *menu = new QMenu(parent);
+ auto action = menu->addAction(i18n("Code Navigation Mode"));
+ action->setIcon(QIcon::fromTheme(u"debug-step-into"_s));
+ menu->menuAction()->setIcon(action->icon());
+ menu->menuAction()->setText(i18n("Code Navigation"));
+ menu->menuAction()->setObjectName(u"rkcodenav_menu"_s);
+ action->setObjectName(u"rkcodenav"_s);
+ _actions.append(menu->menuAction());
+ _actions.append(action);
+ connect(action, &QAction::triggered, parent, [this, parent]() {
+ if (!internal) {
+ internal = new RKCodeNavigationInternal(this->view, this);
+ }
+ auto w = new RKCodeNavigationWidget(parent, internal);
w->show();
w->updatePos();
w->input->setFocus();
- }
-
- void singleShotNavigation(KTextEditor::View *view, const QChar &command) {
- RKCodeNavigationBase w(view);
- w.navigateBase(w.handleCommandBase(command));
- }
-
- QAction *directNavigationAction(const QString &label, KTextEditor::View *view, const QChar command) {
- QAction *a = new QAction(label);
- QObject::connect(a, &QAction::triggered, view, [view, command]() { singleShotNavigation(view, command); });
- return a;
- }
-
- QMenu *actionMenu(KTextEditor::View *view, QWidget *parent) {
- QMenu *menu = new QMenu(parent);
- auto action = menu->addAction(i18n("Code Navigation Mode"));
- action->setIcon(QIcon::fromTheme(u"debug-step-into"_s));
- menu->menuAction()->setIcon(action->icon());
- menu->menuAction()->setText(i18n("Code Navigation"));
- QObject::connect(action, &QAction::triggered, parent, [view, parent]() { doNavigation(view, parent); });
-
- menu->addSeparator();
- QObject::connect(menu, &QMenu::aboutToShow, menu, [view, menu]() {
- bool rmdmode = view->document()->highlightingMode() == u"R Markdown"_s;
- // TODO: Allow setting and customizing shortcuts
- menu->addAction(directNavigationAction(i18n("Next statement"), view, u'n'));
- menu->addAction(directNavigationAction(i18n("Previous statement"), view, u'N'));
- menu->addAction(directNavigationAction(i18n("Next (inner) statement"), view, u'i'));
- menu->addAction(directNavigationAction(i18n("Previous (inner) statement"), view, u'P'));
- menu->addAction(directNavigationAction(i18n("Next outer statement"), view, u'o'));
- menu->addAction(directNavigationAction(i18n("Previous outer statement"), view, u'O'));
- menu->addAction(directNavigationAction(i18n("Next toplevel statement"), view, u't'));
- menu->addAction(directNavigationAction(i18n("Previous toplevel statement"), view, u'T'));
- if (rmdmode) {
- menu->addAction(directNavigationAction(i18n("Next code chunk"), view, u'c'));
- menu->addAction(directNavigationAction(i18n("Previous code chunk"), view, u'C'));
- }
- menu->addSeparator();
- menu->addAction(directNavigationAction(i18n("Select current statement"), view, u's'));
- if (rmdmode) {
- menu->addAction(directNavigationAction(i18n("Select current code chunk"), view, u'S'));
- }
- });
-
- QObject::connect(menu, &QMenu::aboutToHide, menu, [menu]() {
- const auto acts = menu->actions();
- for (int i = acts.size() - 1; i >= 2; --i) {
- acts[i]->deleteLater();
- }
- });
-
- return menu;
- }
-};
+ });
+
+ rmdactions = new QActionGroup(this);
+
+ menu->addSeparator();
+
+ menu->addSection(i18nc("verb: To jump to a different place in the code", "Go"));
+ addAction(menu, u"rkcodenav_next"_s, i18n("Next statement"), u'n');
+ addAction(menu, u"rkcodenav_prev"_s, i18n("Previous statement"), u'N');
+ addAction(menu, u"rkcodenav_inner"_s, i18n("Next (inner) statement"), u'i');
+ addAction(menu, u"rkcodenav_prev_inner"_s, i18n("Previous (inner) statement"), u'P');
+ addAction(menu, u"rkcodenav_outer"_s, i18n("Next outer statement"), u'o');
+ addAction(menu, u"rkcodenav_prev_outer"_s, i18n("Previous outer statement"), u'O');
+ addAction(menu, u"rkcodenav_toplevel"_s, i18n("Next toplevel statement"), u't');
+ addAction(menu, u"rkcodenav_prev_toplevel"_s, i18n("Previous toplevel statement"), u'T');
+
+ rmdactions->addAction(addAction(menu, u"rkcodenav_chunk"_s, i18n("Next code chunk"), u'c'));
+ rmdactions->addAction(addAction(menu, u"rkcodenav_prev_chunk"_s, i18n("Previous code chunk"), u'C'));
+ menu->addSection(i18n("Select"));
+ addAction(menu, u"rkcodenav_select"_s, i18n("Select current statement"), u's');
+ rmdactions->addAction(addAction(menu, u"rkcodenav_select_chunk"_s, i18n("Select current code chunk"), u'S'));
+
+ QObject::connect(menu, &QMenu::aboutToShow, this, [view, this]() {
+ bool rmdmode = view->document()->highlightingMode() == u"R Markdown"_s;
+ rmdactions->setVisible(rmdmode);
+ });
+}
+
+QAction *RKCodeNavigation::addAction(QMenu *menu, const QString &name, const QString &label, const QChar command) {
+ QAction *a = new QAction(label);
+ a->setObjectName(name);
+ QObject::connect(a, &QAction::triggered, view, [this, command]() {
+ if (!internal) {
+ internal = new RKCodeNavigationInternal(view, this);
+ }
+ internal->navigate(internal->handleCommand(command));
+ });
+ menu->addAction(a);
+ _actions.append(a);
+ return a;
+}
diff --git a/rkward/windows/rkcodenavigation.h b/rkward/windows/rkcodenavigation.h
index 6c33ee1c6..62ac5fe2e 100644
--- a/rkward/windows/rkcodenavigation.h
+++ b/rkward/windows/rkcodenavigation.h
@@ -10,13 +10,24 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <KTextEditor/View>
+class QAction;
+class QActionGroup;
class QMenu;
+class RKCodeNavigationInternal;
-namespace RKCodeNavigation {
- void doNavigation(KTextEditor::View *view, QWidget *parent);
-/** creates a menu code navigation actions.
- * The first action in the menu brings up code navigation mode (may e.g. be plugged into a toolbar). */
- QMenu *actionMenu(KTextEditor::View *view, QWidget *parent);
+class RKCodeNavigation : public QObject {
+ public:
+ RKCodeNavigation(KTextEditor::View *view, QWidget *parent);
+
+ /** get list of (named; suitable for use with KActionCollection) actions for code navigation. */
+ QList<QAction *> actions() const { return _actions; };
+ private:
+ QAction *addAction(QMenu *menu, const QString &name, const QString &label, const QChar command);
+
+ KTextEditor::View *view;
+ QList<QAction *> _actions;
+ RKCodeNavigationInternal *internal;
+ QActionGroup *rmdactions;
};
#endif
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index 112fcd4cd..d59017b9f 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -403,10 +403,9 @@ void RKCommandEditorWindow::initializeActions(KActionCollection *ac) {
file_save_as_action = findAction(m_view, QStringLiteral("file_save_as"));
if (file_save_as_action) file_save_as_action->setText(i18n("Save Script As..."));
- QMenu *cnmenu = RKCodeNavigation::actionMenu(m_view, this);
- ac->addAction(QStringLiteral("code_navigation_menu"), cnmenu->menuAction());
- action = ac->addAction(QStringLiteral("code_navigation"), cnmenu->actions().first());
- ac->setDefaultShortcuts(action, QList<QKeySequence>() << (Qt::MetaModifier | Qt::Key_N));
+ auto rkcn = new RKCodeNavigation(m_view, this);
+ ac->addActions(rkcn->actions());
+ ac->setDefaultShortcuts(ac->action(u"rkcodenav"_s), QList<QKeySequence>() << (Qt::MetaModifier | Qt::Key_N));
const auto actions = standardActionCollection()->actions() + ac->actions();
for (QAction *action : actions) {
diff --git a/rkward/windows/rkcommandeditorwindowpart.rc b/rkward/windows/rkcommandeditorwindowpart.rc
index 35ff1ddf4..60acbb6ba 100644
--- a/rkward/windows/rkcommandeditorwindowpart.rc
+++ b/rkward/windows/rkcommandeditorwindowpart.rc
@@ -4,7 +4,7 @@ SPDX-FileCopyrightText: by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemai
SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
-->
-<kpartgui name="rkward_commandeditor" version="822">
+<kpartgui name="rkward_commandeditor" version="823">
<MenuBar>
<Menu name="file"><text>&File</text></Menu> <!-- Define placeholder to work around menu ordering bug -->
<Menu name="edit"><text>&Edit</text>
@@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Action name="render_preview" group="postrun_actions_merge"/>
<Separator group="postrun_actions_merge"/>
<Action name="setwd_to_script" group="postrun_actions_merge"/>
- <Action name="code_navigation_menu" group="postrun_actions_merge"/>
+ <Action name="rkcodenav_menu" group="postrun_actions_merge"/>
</Menu>
<Menu name="settings"><text>&Settings</text>
<Action name="configure_commandeditor"></Action>
@@ -28,14 +28,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ToolBar fullWidth="true" name="mainToolBar">
<Action name="run_block" group="posttoolbar_actions_merge"/>
- <Action name="code_navigation" group="posttoolbar_actions_merge"/>
+ <Action name="rkcodenav" group="posttoolbar_actions_merge"/>
<Action name="render_preview" group="posttoolbar_actions_merge"/>
<Action name="setwd_to_script" group="posttoolbar_actions_merge"/>
</ToolBar>
<Menu name="ktexteditor_popup">
<Merge/>
- <Action name="code_navigation_menu"/>
+ <Action name="rkcodenav_menu"/>
<Action name="mark_block"/>
<Action name="unmark_block"/>
<Menu name="run"><text>&Run</text>
More information about the rkward-tracker
mailing list