[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 &current_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 &current_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