[education/rkward] rkward/windows: Add basic context menu to qwebview, implement zoom
Thomas Friedrichsmeier
null at kde.org
Thu Apr 9 20:44:24 BST 2026
Git commit b645612c82054ef00ecfd93fdc4ccd9e6a2aa9a5 by Thomas Friedrichsmeier.
Committed on 09/04/2026 at 19:44.
Pushed by tfry into branch 'master'.
Add basic context menu to qwebview, implement zoom
M +2 -1 rkward/windows/rkhtmlviewer.cpp
M +2 -2 rkward/windows/rkhtmlviewer.h
M +14 -21 rkward/windows/rkhtmlwindow.cpp
M +1 -1 rkward/windows/rkhtmlwindow.h
M +10 -5 rkward/windows/rkqwebenginewidget.cpp
M +0 -1 rkward/windows/rkqwebenginewidget.h
M +62 -12 rkward/windows/rkqwebview.cpp
M +2 -1 rkward/windows/rkqwebview.h
M +14 -0 rkward/windows/rkqwebview.js
M +6 -0 rkward/windows/rkqwebview.qml
https://invent.kde.org/education/rkward/-/commit/b645612c82054ef00ecfd93fdc4ccd9e6a2aa9a5
diff --git a/rkward/windows/rkhtmlviewer.cpp b/rkward/windows/rkhtmlviewer.cpp
index 47c356847..0176a625e 100644
--- a/rkward/windows/rkhtmlviewer.cpp
+++ b/rkward/windows/rkhtmlviewer.cpp
@@ -7,12 +7,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "rkhtmlviewer.h"
+#include "rkhtmlwindow.h"
#include "rkqwebenginewidget.h"
#include "rkqwebview.h"
#include "../debug.h"
-RKHTMLViewer::RKHTMLViewer(QObject *parent) : QObject(parent) {
+RKHTMLViewer::RKHTMLViewer(RKHTMLWindow *parent) : QObject(parent), window(parent) {
RK_TRACE(APP);
}
diff --git a/rkward/windows/rkhtmlviewer.h b/rkward/windows/rkhtmlviewer.h
index 00fdc4c23..d3b578c7e 100644
--- a/rkward/windows/rkhtmlviewer.h
+++ b/rkward/windows/rkhtmlviewer.h
@@ -43,7 +43,6 @@ class RKHTMLViewer : public QObject {
virtual void setScrollPosition(const QPoint &pos, bool wait_for_load) = 0;
// TODO: Could be implemented in base-class as JS call
virtual void findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *found) = 0;
- virtual QMenu *createContextMenu(const QPoint &clickpos) = 0;
virtual QString selectedText() const = 0;
virtual bool supportsContentType(const QString &mimename) = 0;
virtual void zoomIn() = 0;
@@ -54,9 +53,10 @@ class RKHTMLViewer : public QObject {
void selectionChanged(bool has_selection);
void loadFinished();
void navigationRequest(const QUrl ¤t_real_url, const QUrl &requested_url, bool is_new_window);
+ void aboutToShowContextMenu(QMenu *menu);
protected:
- RKHTMLViewer(QObject *parent);
+ RKHTMLViewer(RKHTMLWindow *parent);
RKHTMLWindow *window;
};
diff --git a/rkward/windows/rkhtmlwindow.cpp b/rkward/windows/rkhtmlwindow.cpp
index 06f369228..21c35a105 100644
--- a/rkward/windows/rkhtmlwindow.cpp
+++ b/rkward/windows/rkhtmlwindow.cpp
@@ -72,7 +72,6 @@ RKHTMLWindow::RKHTMLWindow(QWidget *parent, WindowMode mode) : RKMDIWindow(paren
layout->setContentsMargins(0, 0, 0, 0);
page = RKHTMLViewer::getNew(this);
auto view = page->createWidget();
- view->setContextMenuPolicy(Qt::CustomContextMenu);
layout->addWidget(view, 1);
findbar = new RKFindBar(this, true);
findbar->setPrimaryOptions(QList<QWidget *>() << findbar->getOption(RKFindBar::FindAsYouType) << findbar->getOption(RKFindBar::MatchCase));
@@ -106,10 +105,15 @@ RKHTMLWindow::RKHTMLWindow(QWidget *parent, WindowMode mode) : RKMDIWindow(paren
// We have to connect this in order to allow browsing.
connect(page, &RKHTMLViewer::pageInternalNavigation, this, &RKHTMLWindow::internalNavigation);
connect(page, &RKHTMLViewer::navigationRequest, this, [this](const QUrl ¤t_real_url, const QUrl &requested_url, bool is_new_window) {
- openURL(requested_url);
+ if (is_new_window) {
+ RKWorkplace::mainWorkplace()->openAnyUrl(requested_url);
+ } else {
+ openURL(requested_url);
+ }
+ });
+ connect(page, &RKHTMLViewer::aboutToShowContextMenu, this, [this](QMenu *menu) {
+ menu->addAction(part->run_selection);
});
-
- connect(view, &QWidget::customContextMenuRequested, this, &RKHTMLWindow::makeContextMenu);
current_history_position = -1;
url_change_is_from_history = false;
@@ -133,15 +137,6 @@ QUrl RKHTMLWindow::restorableUrl() {
return ::restorableUrl(current_url);
}
-void RKHTMLWindow::makeContextMenu(const QPoint &pos) {
- RK_TRACE(APP);
-
- auto menu = page->createContextMenu(pos);
- menu->addAction(part->run_selection);
- menu->exec(mapToGlobal(pos));
- delete (menu);
-}
-
void RKHTMLWindow::selectionChanged(bool have_selection) {
RK_TRACE(APP);
@@ -159,6 +154,11 @@ void RKHTMLWindow::runSelection() {
RKConsole::pipeUserCommand(page->selectedText());
}
+void RKHTMLWindow::slotCopy() {
+ RK_TRACE(APP);
+ qApp->clipboard()->setText(page->selectedText());
+}
+
void RKHTMLWindow::slotPrint() {
RK_TRACE(APP);
page->print();
@@ -648,14 +648,7 @@ void RKHTMLWindowPart::initActions() {
RK_TRACE(APP);
// common actions
- actionCollection()->addAction(KStandardAction::Copy, QStringLiteral("copy"), window->page, [page = window->page]() {
- page->runJS(u"{ Window.getSelection(); }"_s, [](const QVariant &res) {
- const auto txt = res.toString();
- if (!txt.isEmpty()) {
- qApp->clipboard()->setText(txt);
- }
- });
- });
+ actionCollection()->addAction(KStandardAction::Copy, QStringLiteral("copy"), window, &RKHTMLWindow::slotCopy);
QAction *zoom_in = actionCollection()->addAction(QStringLiteral("zoom_in"), new QAction(QIcon::fromTheme(QStringLiteral("zoom-in")), i18n("Zoom In"), this));
connect(zoom_in, &QAction::triggered, window, &RKHTMLWindow::zoomIn);
QAction *zoom_out = actionCollection()->addAction(QStringLiteral("zoom_out"), new QAction(QIcon::fromTheme(QStringLiteral("zoom-out")), i18n("Zoom Out"), this));
diff --git a/rkward/windows/rkhtmlwindow.h b/rkward/windows/rkhtmlwindow.h
index 51f3edcd6..0452d7e4d 100644
--- a/rkward/windows/rkhtmlwindow.h
+++ b/rkward/windows/rkhtmlwindow.h
@@ -75,6 +75,7 @@ class RKHTMLWindow : public RKMDIWindow {
WindowMode mode() { return window_mode; };
public Q_SLOTS:
+ void slotCopy();
void slotPrint();
void slotExport();
void slotSave();
@@ -98,7 +99,6 @@ class RKHTMLWindow : public RKMDIWindow {
void mimeTypeJobFail(KJob *);
void mimeTypeJobFail2(KJob *);
void internalNavigation(const QUrl &new_url);
- void makeContextMenu(const QPoint &pos);
private:
friend class RKHTMLWindowPart;
diff --git a/rkward/windows/rkqwebenginewidget.cpp b/rkward/windows/rkqwebenginewidget.cpp
index e345f2915..d975129fc 100644
--- a/rkward/windows/rkqwebenginewidget.cpp
+++ b/rkward/windows/rkqwebenginewidget.cpp
@@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <QDir>
#include <QFileDialog>
#include <QFontDatabase>
+#include <QMenu>
#include <QMimeDatabase>
#include <QPrintDialog>
#include <QWebEngineFindTextResult>
@@ -249,6 +250,15 @@ QWidget *RKQWebEngineWidget::createWidget() {
connect(view, &QWebEngineView::selectionChanged, this, [this]() {
Q_EMIT selectionChanged(view->hasSelection());
});
+ view->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(view, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
+ auto v = QWebEngineView::forPage(page);
+ auto menu = v->createStandardContextMenu();
+ Q_EMIT aboutToShowContextMenu(menu);
+ menu->exec(v->mapToGlobal(pos));
+ delete menu;
+ });
+
return view;
}
@@ -371,9 +381,4 @@ void RKQWebEngineWidget::findRequest(const QString &text, bool backwards, RKFind
});
}
-QMenu *RKQWebEngineWidget::createContextMenu(const QPoint &clickpos) {
- RK_TRACE(APP);
- return (QWebEngineView::forPage(page)->createStandardContextMenu());
-}
-
#include "rkqwebenginewidget.moc"
diff --git a/rkward/windows/rkqwebenginewidget.h b/rkward/windows/rkqwebenginewidget.h
index 205cc6308..998384bfe 100644
--- a/rkward/windows/rkqwebenginewidget.h
+++ b/rkward/windows/rkqwebenginewidget.h
@@ -27,7 +27,6 @@ class RKQWebEngineWidget : public RKHTMLViewer {
void setHTML(const QString &html, const QUrl &url) override;
bool installHelpProtocolHandler() override;
void findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *found) override;
- QMenu *createContextMenu(const QPoint &clickpos) override;
QString selectedText() const override;
void exportPage() override;
QPoint scrollPosition() const override;
diff --git a/rkward/windows/rkqwebview.cpp b/rkward/windows/rkqwebview.cpp
index 791973dd9..78f0a67fa 100644
--- a/rkward/windows/rkqwebview.cpp
+++ b/rkward/windows/rkqwebview.cpp
@@ -7,6 +7,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "rkqwebview.h"
+#include <KLocalizedString>
+#include <KStandardActions>
+
+#include <QClipboard>
#include <QFile>
#include <QMenu>
#include <QQuickItem>
@@ -23,11 +27,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
class RKQWebViewCallbackServer {
public:
static void initConnection(RKQWebView *window) {
- static QWebSocketServer *server = nullptr;
if (!server) {
server = new QWebSocketServer(QString(), QWebSocketServer::NonSecureMode);
server->listen(QHostAddress::LocalHost);
- QObject::connect(server, &QWebSocketServer::newConnection, server, [server]() {
+ QObject::connect(server, &QWebSocketServer::newConnection, server, []() {
+ auto server = RKQWebViewCallbackServer::server;
auto con = server->nextPendingConnection();
const auto url = con->requestUrl().url();
auto win = entrypoints.value(url);
@@ -62,10 +66,12 @@ class RKQWebViewCallbackServer {
protected:
static QHash<QString, RKQWebView *> entrypoints;
+ static QWebSocketServer *server;
};
QHash<QString, RKQWebView *> RKQWebViewCallbackServer::entrypoints;
+QWebSocketServer *RKQWebViewCallbackServer::server = nullptr;
-RKQWebView::RKQWebView(RKHTMLWindow *parent) : RKHTMLViewer(parent), view(nullptr), running_script(false) {
+RKQWebView::RKQWebView(RKHTMLWindow *parent) : RKHTMLViewer(parent), view(nullptr), running_script(false), context_menu(nullptr) {
RK_TRACE(APP);
QFile file(u":/js/rkqwebview.js"_s);
if (file.open(QIODevice::ReadOnly)) {
@@ -75,6 +81,11 @@ RKQWebView::RKQWebView(RKHTMLWindow *parent) : RKHTMLViewer(parent), view(nullpt
}
}
+RKQWebView::~RKQWebView() {
+ RK_TRACE(APP);
+ delete context_menu;
+}
+
QUrl RKQWebView::currentAcceptedUrl() const {
return webView()->property("acceptedUrl").toUrl();
}
@@ -147,8 +158,15 @@ void RKQWebView::load(const QUrl &url) {
void RKQWebView::print() {
RK_TRACE(APP);
- // TODO: Doesn't work. Why?
+ // TODO: Doesn't do anything (no error, either). Why?
RKHTMLViewer::runJS(u"window.print()"_s);
+
+ /* This is not really a useful alternative...
+ QPointer<QPrintDialog> dlg(new QPrintDialog(view));
+ if (dlg->exec() == QPrintDialog::Accepted) {
+ view->render(dlg->printer(), QPoint(), QRegion(), QWidget::DrawChildren);
+ }
+ delete dlg; */
}
void RKQWebView::installPersistentJS(const QString &script, const QString &id) {
@@ -204,12 +222,6 @@ void RKQWebView::findRequest(const QString &text, bool backwards, RKFindBar *fin
});
}
-QMenu *RKQWebView::createContextMenu(const QPoint &clickpos) {
- RK_TRACE(APP);
- // TODO
- return new QMenu();
-}
-
QString RKQWebView::selectedText() const {
RK_TRACE(APP);
@@ -229,6 +241,44 @@ void RKQWebView::receivedCallbackMessage(const QString &message) {
}
} else if (msg == "scroll"_L1) {
scroll_pos = QPoint(args["x"_L1].toInt(), args["y"_L1].toInt());
+ } else if (msg == "contextMenu"_L1) {
+ // TODO: some actions we probably want:
+ // back, forward, reload, copy, copy link address, open in new tab, open in external browser,
+ // save image as
+ // TODO most to all of these actions could/should probably be handled in the RKHTMLWindow?
+ if (!context_menu) {
+ context_menu = new QMenu;
+ auto a = new QAction(i18n("Open link in new tab"), this);
+ a->setObjectName("open_new");
+ connect(a, &QAction::triggered, this, [a, this]() {
+ Q_EMIT navigationRequest(url(), QUrl(a->property("_url").toString()), true);
+ });
+ context_menu->addAction(a);
+
+ // NOTE copy is already present as an action in the main window, but we want a separate
+ // one that we can show/hide
+ a = KStandardActions::copy(window, &RKHTMLWindow::slotCopy, this);
+ a->setObjectName("copy");
+ context_menu->addAction(a);
+
+ a = new QAction(i18n("Copy link address"), this);
+ connect(a, &QAction::triggered, this, [a]() {
+ qApp->clipboard()->setText(a->property("_url").toString());
+ });
+ a->setObjectName("copy_link");
+ context_menu->addAction(a);
+ }
+ const auto url = args["url"_L1].toString();
+ auto a = findChild<QAction *>("open_new", Qt::FindDirectChildrenOnly);
+ a->setVisible(!url.isEmpty());
+ a->setProperty("_url", url);
+ a = findChild<QAction *>("copy_link", Qt::FindDirectChildrenOnly);
+ a->setVisible(!url.isEmpty());
+ a->setProperty("_url", url);
+ a = findChild<QAction *>("copy", Qt::FindDirectChildrenOnly);
+ a->setVisible(!selected_text.isEmpty());
+ Q_EMIT aboutToShowContextMenu(context_menu);
+ context_menu->exec((QPoint(args["x"_L1].toInt(), args["y"_L1].toInt()) - scroll_pos));
} else if (msg == "pageInternalNav"_L1) {
Q_EMIT pageInternalNavigation(QUrl(args["href"_L1].toString()));
} else {
@@ -259,10 +309,10 @@ bool RKQWebView::supportsContentType(const QString &mimename) {
void RKQWebView::zoomIn() {
RK_TRACE(APP);
- // TODO
+ QMetaObject::invokeMethod(webView(), "doZoom", Q_ARG(int, 1));
}
void RKQWebView::zoomOut() {
RK_TRACE(APP);
- // TODO
+ QMetaObject::invokeMethod(webView(), "doZoom", Q_ARG(int, -1));
}
diff --git a/rkward/windows/rkqwebview.h b/rkward/windows/rkqwebview.h
index d37a8c126..069b0982f 100644
--- a/rkward/windows/rkqwebview.h
+++ b/rkward/windows/rkqwebview.h
@@ -31,7 +31,6 @@ class RKQWebView : public RKHTMLViewer {
void setHTML(const QString &html, const QUrl &url) override;
bool installHelpProtocolHandler() override;
void findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *found) override;
- QMenu *createContextMenu(const QPoint &clickpos) override;
QString selectedText() const override;
void exportPage() override;
QPoint scrollPosition() const override;
@@ -44,6 +43,7 @@ class RKQWebView : public RKHTMLViewer {
friend class RKHTMLViewer;
friend class RKQWebViewCallbackServer;
RKQWebView(RKHTMLWindow *parent);
+ virtual ~RKQWebView();
QPointer<QQuickWidget> view;
QUrl currentAcceptedUrl() const;
QMap<QString, QString> persistentScripts;
@@ -57,6 +57,7 @@ class RKQWebView : public RKHTMLViewer {
void receivedCallbackMessage(const QString &message);
QString selected_text;
QPoint scroll_pos;
+ QMenu *context_menu;
private Q_SLOTS:
void onUrlChanged(const QUrl &url, const QString &error, int status);
void onLoadFinished(const QUrl &url);
diff --git a/rkward/windows/rkqwebview.js b/rkward/windows/rkqwebview.js
index 79b465563..99cf45a95 100644
--- a/rkward/windows/rkqwebview.js
+++ b/rkward/windows/rkqwebview.js
@@ -31,4 +31,18 @@ document.addEventListener('click', e => {
const origin = e.target.closest('a');
if (origin && origin.href.startswith('#')) {
__rkward_sendMessage("pageInternalNav", { url: origin.href });
+ e.preventDefault();
+ }
+});
+
+document.addEventListener('contextmenu', e => {
+ let params = { x: e.screenX, y: e.screenY };
+ try {
+ params.url = e.target.closest('a').href;
+ } catch {}
+ try {
+ params.src = e.target.closest('img').src;
+ } catch {}
+ __rkward_sendMessage("contextMenu", params);
+ e.preventDefault();
});
diff --git a/rkward/windows/rkqwebview.qml b/rkward/windows/rkqwebview.qml
index ca84b899d..4c7983dd3 100644
--- a/rkward/windows/rkqwebview.qml
+++ b/rkward/windows/rkqwebview.qml
@@ -25,6 +25,7 @@ Item {
visible: true
width: parent.width
height: parent.height
+ transformOrigin: Item.TopLeft
property string acceptedUrl: ""
property string requestedUrl: ""
@@ -46,5 +47,10 @@ Item {
loadFinished(request.url);
}
}
+ function doZoom(dir: int) {
+ scale += dir * .1;
+ width = parent.width / scale;
+ height = parent.height / scale;
+ }
}
}
More information about the rkward-tracker
mailing list