[education/rkward] rkward: Add code navigation actions to context menu
Thomas Friedrichsmeier
null at kde.org
Mon May 26 15:06:38 BST 2025
Git commit 839dbff32f0d38e14bfe7db27751dccad729ec64 by Thomas Friedrichsmeier.
Committed on 26/05/2025 at 14:06.
Pushed by tfry into branch 'master'.
Add code navigation actions to context menu
M +1 -1 rkward/autotests/data/script1.Rmd
M +187 -117 rkward/windows/rkcodenavigation.cpp
M +5 -1 rkward/windows/rkcodenavigation.h
M +3 -9 rkward/windows/rkcommandeditorwindow.cpp
M +1 -2 rkward/windows/rkcommandeditorwindow.h
M +3 -2 rkward/windows/rkcommandeditorwindowpart.rc
https://invent.kde.org/education/rkward/-/commit/839dbff32f0d38e14bfe7db27751dccad729ec64
diff --git a/rkward/autotests/data/script1.Rmd b/rkward/autotests/data/script1.Rmd
index 70540faf2..6f8a981de 100644
--- a/rkward/autotests/data/script1.Rmd
+++ b/rkward/autotests/data/script1.Rmd
@@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
This is markdown ``{r} .some <- `markdown` `` and this is too, with some `inline(code)`.
-Usually, there would be lot of mardown (and this may contain many relevant markers, such as {}, and []).
+Usually, there would be lot of markdown (and this may contain many relevant markers, such as {}, and []).
```{r echo=TRUE}
symb01 <-symb02()
diff --git a/rkward/windows/rkcodenavigation.cpp b/rkward/windows/rkcodenavigation.cpp
index 5379db689..639503e93 100644
--- a/rkward/windows/rkcodenavigation.cpp
+++ b/rkward/windows/rkcodenavigation.cpp
@@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <QFrame>
#include <QKeyEvent>
#include <QLabel>
+#include <QMenu>
#include <QPainter>
#include <QVBoxLayout>
@@ -25,119 +26,37 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "../debug.h"
-class RKCodeNavigationWidget : public QFrame {
+class RKCodeNavigationBase {
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();
- }
+ RKCodeNavigationBase(KTextEditor::View *view) :
+ view(view),
+ doc(view->document()),
+ rmdmode(doc->highlightingMode() == u"R Markdown"_s),
+ ps(doc->text(), rmdmode) {
+ };
struct StoredPosition {
- QString query;
int pos;
KTextEditor::Range selection;
- bool hit_top;
- bool hit_bottom;
QChar command;
+ QString message;
};
- 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();
+ StoredPosition viewPosition() const {
+ StoredPosition pos;
+ // translate cursor position to string index
+ pos.pos = cursorToPosition(view->cursorPosition());
+ pos.selection = view->selectionRange();
+ return pos;
}
- void handleCommand(const QChar command) {
- RK_TRACE(APP);
- // start from last known position
- auto last = stored_positions.last();
+ StoredPosition handleCommandBase(const QChar &command) const {
+ auto last = viewPosition();
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);
@@ -171,43 +90,146 @@ class RKCodeNavigationWidget : public QFrame {
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;
+ newpos.message = i18n("Command 'S' is for R Markdown, only");
+ return newpos;
}
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;
+ newpos.message = i18n("Unknown command <tt><b>%1</b></tt>").arg(command);
+ return newpos;
}
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."));
+ newpos.message = 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."));
+ newpos.message = 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."));
+ newpos.message = 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."));
+ newpos.message = i18nc("Keep this short", "Search hit top.<br/><tt><b>!</b></tt> to move to bottom.");
}
}
+ }
+
+ return newpos;
+ }
+
+ void navigateBase(const StoredPosition &newpos) const {
+ 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 {
+ view->setCursorPosition(positionToCursor(newpos.pos));
+ view->setSelection(KTextEditor::Range(-1, -1, -1, -1));
+ }
+ }
+
+ int cursorToPosition(const KTextEditor::Cursor &cursor) const {
+ 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) const {
+ 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();
+ }
+
+ protected:
+ KTextEditor::View *view;
+ KTextEditor::Document *doc;
+ bool rmdmode;
+ RKParsedScript ps;
+};
+
+class RKCodeNavigationWidget : public QFrame, public RKCodeNavigationBase {
+ public:
+ RKCodeNavigationWidget(KTextEditor::View *view, QWidget *parent) : QFrame(parent, Qt::Popup | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint), RKCodeNavigationBase(view) {
+ 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);
+
+ stored_size = size();
+ stored_positions.append(viewPosition());
+ updateLabel();
+ }
+
+ void updatePos() {
+ RK_TRACE(APP);
+ move(view->mapToGlobal(view->geometry().topRight() - QPoint(width() + 5, -5)));
+ }
+
+ void focusOutEvent(QFocusEvent *) override {
+ RK_TRACE(APP);
+ deleteLater();
+ }
+
+ void navigate(const StoredPosition &newpos) {
+ RK_TRACE(APP);
+ navigateBase(newpos);
+ if (message->isVisible()) {
+ message->hide();
+ QTimer::singleShot(0, this, [this]() {
+ resize(stored_size);
+ updatePos();
+ });
+ }
+ updateLabel();
+ }
+
+ void handleCommand(const QChar command) {
+ RK_TRACE(APP);
+
+ auto pos = handleCommandBase(command);
+ if (!pos.message.isEmpty()) {
message->show();
updatePos();
return;
}
- stored_positions.append(newpos);
- navigate(newpos);
+ stored_positions.append(pos);
+ navigate(pos);
}
bool eventFilter(QObject *from, QEvent *event) override {
@@ -258,14 +280,10 @@ class RKCodeNavigationWidget : public QFrame {
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;
+ QList<StoredPosition> stored_positions;
};
namespace RKCodeNavigation {
@@ -275,4 +293,56 @@ namespace RKCodeNavigation {
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;
+ }
};
diff --git a/rkward/windows/rkcodenavigation.h b/rkward/windows/rkcodenavigation.h
index 3b93238c9..6c33ee1c6 100644
--- a/rkward/windows/rkcodenavigation.h
+++ b/rkward/windows/rkcodenavigation.h
@@ -10,9 +10,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <KTextEditor/View>
+class QMenu;
+
namespace RKCodeNavigation {
void doNavigation(KTextEditor::View *view, QWidget *parent);
- void addMenuEntries(QMenu *menu);
+/** 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);
};
#endif
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index e7e12f55f..112fcd4cd 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -255,8 +255,6 @@ RKCommandEditorWindow::RKCommandEditorWindow(QWidget *parent, const QUrl &_url,
connect(m_doc, &KTextEditor::Document::modifiedChanged, this, &RKCommandEditorWindow::autoSaveHandlerModifiedChanged);
connect(m_doc, &KTextEditor::Document::textChanged, this, &RKCommandEditorWindow::textChanged);
connect(m_view, &KTextEditor::View::selectionChanged, this, &RKCommandEditorWindow::selectionChanged);
- // somehow the katepart loses the context menu each time it loses focus
- connect(m_view, &KTextEditor::View::focusIn, this, &RKCommandEditorWindow::focusIn);
if (use_r_highlighting) {
RKCommandHighlighter::setHighlighting(m_doc, RKCommandHighlighter::RScript);
@@ -405,8 +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..."));
- action = ac->addAction(QStringLiteral("code_navigation"), this, [this]() { RKCodeNavigation::doNavigation(m_view, this); });
- action->setText(i18n("Code Navigation"));
+ 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));
const auto actions = standardActionCollection()->actions() + ac->actions();
@@ -476,11 +475,6 @@ void RKCommandEditorWindow::initBlocks() {
RK_ASSERT(block_records.size() == NUM_BLOCK_RECORDS);
}
-void RKCommandEditorWindow::focusIn(KTextEditor::View *v) {
- RK_TRACE(COMMANDEDITOR);
- RK_ASSERT(v == m_view);
-}
-
QString RKCommandEditorWindow::fullCaption() {
RK_TRACE(COMMANDEDITOR);
diff --git a/rkward/windows/rkcommandeditorwindow.h b/rkward/windows/rkcommandeditorwindow.h
index 44b256a13..dcb0cdcd5 100644
--- a/rkward/windows/rkcommandeditorwindow.h
+++ b/rkward/windows/rkcommandeditorwindow.h
@@ -1,6 +1,6 @@
/*
rkcommandeditorwindow - This file is part of the RKWard project. Created: Mon Aug 30 2004
-SPDX-FileCopyrightText: 2004-2022 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+SPDX-FileCopyrightText: 2004-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
*/
@@ -108,7 +108,6 @@ class RKCommandEditorWindow : public RKMDIWindow, public RKScriptContextProvider
public Q_SLOTS:
/** update Tab caption according to the current url. Display the filename-component of the URL, or - if not available - a more elaborate description of the url. Also appends a "[modified]" if appropriate */
void updateCaption();
- void focusIn(KTextEditor::View *);
/** run the currently selected command(s) or line */
void runCurrent();
/** run the entire script */
diff --git a/rkward/windows/rkcommandeditorwindowpart.rc b/rkward/windows/rkcommandeditorwindowpart.rc
index c016e3038..35ff1ddf4 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="821">
+<kpartgui name="rkward_commandeditor" version="822">
<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" group="postrun_actions_merge"/>
+ <Action name="code_navigation_menu" group="postrun_actions_merge"/>
</Menu>
<Menu name="settings"><text>&Settings</text>
<Action name="configure_commandeditor"></Action>
@@ -35,6 +35,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Menu name="ktexteditor_popup">
<Merge/>
+ <Action name="code_navigation_menu"/>
<Action name="mark_block"/>
<Action name="unmark_block"/>
<Menu name="run"><text>&Run</text>
More information about the rkward-tracker
mailing list