[education/rkward] rkward: Implement more functions in rkqwebview

Thomas Friedrichsmeier null at kde.org
Thu Apr 9 20:44:24 BST 2026


Git commit 0e42417f5704dcb88a41e7c450db15cff88f891f by Thomas Friedrichsmeier.
Committed on 09/04/2026 at 19:44.
Pushed by tfry into branch 'master'.

Implement more functions in rkqwebview

M  +1    -1    rkward/main.cpp
M  +1    -1    rkward/windows/rkhtmlwindow.cpp
M  +61   -22   rkward/windows/rkqwebview.cpp
M  +10   -2    rkward/windows/rkqwebview.h
M  +17   -9    rkward/windows/rkqwebview.qml
M  +7    -1    rkward/windows/rkworkplace.cpp
M  +3    -0    rkward/windows/rkworkplace.h

https://invent.kde.org/education/rkward/-/commit/0e42417f5704dcb88a41e7c450db15cff88f891f

diff --git a/rkward/main.cpp b/rkward/main.cpp
index c69dbb753..61b494637 100644
--- a/rkward/main.cpp
+++ b/rkward/main.cpp
@@ -154,8 +154,8 @@ int main(int argc, char *argv[]) {
 	scheme.setFlags(QWebEngineUrlScheme::LocalAccessAllowed);
 	QWebEngineUrlScheme::registerScheme(scheme);
 	BreezeIcons::initIcons(); // install as fallback theme. Too many issues with missing icons, otherwise
-	QtWebView::initialize();
 	QApplication app(argc, argv);
+	QtWebView::initialize();
 	KDSingleApplication app_singleton;
 #if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
 	// Follow the example of kate, and use breeze theme on Windows and Mac, which appears to work best
diff --git a/rkward/windows/rkhtmlwindow.cpp b/rkward/windows/rkhtmlwindow.cpp
index a6d5d3984..06f369228 100644
--- a/rkward/windows/rkhtmlwindow.cpp
+++ b/rkward/windows/rkhtmlwindow.cpp
@@ -89,7 +89,7 @@ RKHTMLWindow::RKHTMLWindow(QWidget *parent, WindowMode mode) : RKMDIWindow(paren
 	                              .arg(scheme->foreground().color().name(), scheme->background().color().name(),
 	                                   scheme->foreground(KColorScheme::VisitedText).color().name(), scheme->foreground(KColorScheme::LinkText).color().name(),
 	                                   scheme->shade(KColorScheme::MidShade).name());
-	page->installPersistentJS(u"fix_color_scheme"_s, color_scheme_js);
+	page->installPersistentJS(color_scheme_js, u"fix_color_scheme"_s);
 	page->installHelpProtocolHandler();
 
 	layout->addWidget(findbar);
diff --git a/rkward/windows/rkqwebview.cpp b/rkward/windows/rkqwebview.cpp
index 1d40cd158..e6d7eeaa1 100644
--- a/rkward/windows/rkqwebview.cpp
+++ b/rkward/windows/rkqwebview.cpp
@@ -11,7 +11,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <QQuickItem>
 #include <QQuickWidget>
 
+#include "../misc/rkfindbar.h"
 #include "rkhtmlwindow.h"
+#include "rkworkplace.h"
 
 #include "../debug.h"
 
@@ -19,38 +21,54 @@ RKQWebView::RKQWebView(RKHTMLWindow *parent) : RKHTMLViewer(parent), view(nullpt
 	RK_TRACE(APP);
 }
 
-void RKQWebView::onPageLoad(const QUrl &_url, const QString &error, int status) {
-	RK_DEBUG(APP, DL_WARNING, "loading %s %d", qPrintable(_url.toString()), status);
-	/* TODO: we may need to inject a script similar for handling target=new
-	    and page internal navigation?
-	RKHTMLViewer::runJS(u"document.addEventListener('click', e => {"
-	    "  const origin = e.target.closest('a');"
-	    "  if (origin) alert(origin.href);"
-	    "})\n"_s); */
-	if (status == 0) {
-		bool new_window = false;
-		Q_EMIT navigationRequest(webView()->property("acceptedUrl").toUrl(), _url, new_window);
-		// we may be redirected via the above signal
-		if (_url != url()) {
-			RK_DEBUG(APP, DL_DEBUG, "redirecting %s to %s", qPrintable(_url.toString()), qPrintable(url().toString()));
-		}
-	} else if (status == 2) {
-		Q_EMIT loadFinished();
+QUrl RKQWebView::currentAcceptedUrl() const {
+	return webView()->property("acceptedUrl").toUrl();
+}
+
+void RKQWebView::onUrlChanged(const QUrl &_url, const QString &error, int status) {
+	RK_DEBUG(APP, DL_DEBUG, "webview url changed to %s status %d, msg %s", qPrintable(_url.toString()), status, qPrintable(error));
+	bool new_window = false;
+	Q_EMIT navigationRequest(currentAcceptedUrl(), _url, new_window);
+	// we may be redirected via the above signal
+	if (_url != url()) {
+		RK_DEBUG(APP, DL_DEBUG, "redirecting %s to %s", qPrintable(_url.toString()), qPrintable(url().toString()));
+	}
+	if (status == 0 && _url.scheme() == u"rkward") {
+		// Some QWebView plugins will also trigger openRKWardURl() via QDesktopServices::setUrlHandler()
+		// Others won't. In either case, we've handled it from here.
+		RKWorkplace::mainWorkplace()->suppressRKWardUrlHandling(_url);
+	}
+}
+
+void RKQWebView::onLoadFinished(const QUrl &url) {
+	RK_TRACE(APP);
+	RK_ASSERT(currentAcceptedUrl() == url);
+	for (const auto &script : std::as_const(persistentScripts)) {
+		RKHTMLViewer::runJS(script);
 	}
+	Q_EMIT loadFinished();
 }
 
 QWidget *RKQWebView::createWidget() {
 	RK_TRACE(APP);
 	view = new QQuickWidget();
-	view->setSource(QUrl(QStringLiteral("qrc:/qml/rkqwebview.qml")));
 	view->setResizeMode(QQuickWidget::SizeRootObjectToView);
-	connect(webView(), SIGNAL(loaded(const QUrl &, const QString &, int)), this, SLOT(onPageLoad(const QUrl &, const QString &, int)));
+	view->setSource(QUrl(QStringLiteral("qrc:/qml/rkqwebview.qml")));
+	connect(webView(), SIGNAL(pageUrlChanged(const QUrl &, const QString &, int)), this, SLOT(onUrlChanged(const QUrl &, const QString &, int)));
+	connect(webView(), SIGNAL(loadFinished(const QUrl &)), this, SLOT(onLoadFinished(const QUrl &)));
+	connect(webView(), SIGNAL(runJSResult(const QVariant &)), this, SLOT(onRunJSResult(const QVariant &)));
+	/* TODO: we may need to inject a script similar for handling target=new
+	    and page internal navigation?
+	RKHTMLViewer::runJS(u"document.addEventListener('click', e => {"
+	    "  const origin = e.target.closest('a');"
+	    "  if (origin) alert(origin.href);"
+	    "})\n"_s); */
 	return view;
 }
 
 QObject *RKQWebView::webView() const {
-	// TODO: we have some premature calls to this function. Find and fix them
 	if (!view) {
+		// could happen (happened in the past) due to premature calls to this function.
 		RK_ASSERT(view);
 		return nullptr;
 	}
@@ -77,20 +95,31 @@ void RKQWebView::load(const QUrl &url) {
 
 void RKQWebView::print() {
 	RK_TRACE(APP);
+	// TODO: Doesn't work. Why?
 	RKHTMLViewer::runJS(u"window.print()"_s);
 }
 
 void RKQWebView::installPersistentJS(const QString &script, const QString &id) {
 	RK_TRACE(APP);
+	persistentScripts[id] = script;
 }
 
 void RKQWebView::runJS(const QString &script, std::function<void(const QVariant &)> callback) {
 	RK_TRACE(APP);
-	QMetaObject::invokeMethod(webView(), "runJavaScript", Q_ARG(QString, script), Q_ARG(QJSValue, QJSValue()));
+	RK_ASSERT(runjs_callback == nullptr); // no nested calls, please!
+	runjs_callback = callback;
+	QMetaObject::invokeMethod(webView(), "runJSWrapper", Q_ARG(QString, script));
+}
+
+void RKQWebView::onRunJSResult(const QVariant &result) {
+	RK_TRACE(APP);
+	if (runjs_callback != nullptr) runjs_callback(result);
+	runjs_callback = std::function<void(const QVariant &)>();
 }
 
-void RKQWebView::setHTML(const QString &html, const QUrl &url) {
+void RKQWebView::setHTML(const QString &html, const QUrl &_url) {
 	RK_TRACE(APP);
+	QMetaObject::invokeMethod(webView(), "loadHtml", Q_ARG(QString, html), Q_ARG(QUrl, _url));
 }
 
 bool RKQWebView::installHelpProtocolHandler() {
@@ -100,6 +129,16 @@ bool RKQWebView::installHelpProtocolHandler() {
 
 void RKQWebView::findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *found) {
 	RK_TRACE(APP);
+
+	*found = true; // real result is not available, synchronously
+	auto _txt = text;
+	// NOTE: window.find() is marked as not standardized at the time of this writing!
+	QString script = u"window.find('%1', %2, %3, true)"_s.arg(_txt.replace(u"'"_s, u"\\'"_s),
+	                                                          (findbar->isOptionSet(RKFindBar::MatchCase) ? u"true"_s : u"false"_s),
+	                                                          (backwards ? u"true"_s : u"false"_s));
+	runJS(script, [findbar](const QVariant &res) {
+		if (!res.toBool()) findbar->indicateSearchFail();
+	});
 }
 
 QMenu *RKQWebView::createContextMenu(const QPoint &clickpos) {
diff --git a/rkward/windows/rkqwebview.h b/rkward/windows/rkqwebview.h
index 48511ed09..f681105c5 100644
--- a/rkward/windows/rkqwebview.h
+++ b/rkward/windows/rkqwebview.h
@@ -8,6 +8,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #ifndef RKQWEBVIEW_H
 #define RKQWEBVIEW_H
 
+#include <QMap>
 #include <QPointer>
 
 #include "rkhtmlviewer.h"
@@ -15,6 +16,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
 class QQuickWidget;
 class QQuickItem;
 
+/** Interface abstraction for QWebView. Note that QWebView is essentially QML only until Qt 6.10. Once
+ *  we can assume Qt 6.11,some simplifications will be possible. */
 class RKQWebView : public RKHTMLViewer {
 	Q_OBJECT
   public:
@@ -23,7 +26,7 @@ class RKQWebView : public RKHTMLViewer {
 	void load(const QUrl &url) override;
 	void print() override;
 	void installPersistentJS(const QString &script, const QString &id) override;
-	void runJS(const QString &script, std::function<void(const QVariant &)> callback) override;
+	void runJS(const QString &script, std::function<void(const QVariant &)> callback = std::function<void(const QVariant &)>()) override;
 	void setHTML(const QString &html, const QUrl &url) override;
 	bool installHelpProtocolHandler() override;
 	void findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *found) override;
@@ -40,8 +43,13 @@ class RKQWebView : public RKHTMLViewer {
 	friend class RKHTMLViewer;
 	RKQWebView(RKHTMLWindow *parent);
 	QPointer<QQuickWidget> view;
+	QUrl currentAcceptedUrl() const;
+	QMap<QString, QString> persistentScripts;
+	std::function<void(const QVariant &)> runjs_callback;
   private Q_SLOTS:
-	void onPageLoad(const QUrl &url, const QString &error, int status);
+	void onUrlChanged(const QUrl &url, const QString &error, int status);
+	void onLoadFinished(const QUrl &url);
+	void onRunJSResult(const QVariant &result);
 	QObject *webView() const;
 };
 
diff --git a/rkward/windows/rkqwebview.qml b/rkward/windows/rkqwebview.qml
index 1059a24ab..1efc367a2 100644
--- a/rkward/windows/rkqwebview.qml
+++ b/rkward/windows/rkqwebview.qml
@@ -13,7 +13,13 @@ Item {
         visible: true
 
         WebView {
-                signal loaded(url murl, string error, int status)
+                signal pageUrlChanged(url murl, string error, int status)
+                signal loadFinished(url murl)
+                signal runJSResult(variant result)
+
+                function runJSWrapper(script: string) {
+                        runJavaScript(script, runJSResult)
+                }
                 id: webView
                 objectName: "webView"
                 visible: true
@@ -22,14 +28,16 @@ Item {
                 property string acceptedUrl: ""
 
                 onLoadingChanged: request => {
-                        // give the frontend a chance to intervene, e.g. rewriting rkward:// url, opening in new window, 
-                        // denying external urls, etc.
-                        loaded(request.url, request.error, request.status);
-                        if (request.status == WebView.LoadStartedStatus) {
-                                if (request.url != acceptedUrl) {
-                                        //console.log("Navigation request to " + request.url + "denied");
-                                        url = acceptedUrl;
-                                }
+                        // notify frontend of any url changes, giving it a chance to intervene (e.g. rewriting
+                        // rkward:// urls etc.)
+                        // Note that the semantics of just when loadingChanged is emitted differ between the QWebView
+                        // plugins. E.g. for webengine, we get a LoadStartedStatus, then LoadFailedStatus for rkward://-urls,
+                        // while for webview2, we only get LoadFailedStatus.
+                        if (request.url != acceptedUrl) {
+                                pageUrlChanged(request.url, request.error, request.status);
+                        }
+                        if (request.status == WebView.LoadSucceededStatus) {
+                                loadFinished(request.url);
                         }
                 }
         }
diff --git a/rkward/windows/rkworkplace.cpp b/rkward/windows/rkworkplace.cpp
index 369cd9f0b..ba7ed1de6 100644
--- a/rkward/windows/rkworkplace.cpp
+++ b/rkward/windows/rkworkplace.cpp
@@ -403,7 +403,13 @@ void RKWorkplace::namedWindowOwnerDestroyed(QObject *owner) {
 void RKWorkplace::openRKWardUrl(const QUrl &url) {
 	RK_TRACE(APP);
 	RK_ASSERT(url.scheme() == QLatin1String("rkward"));
-	RKHTMLWindow::handleRKWardURL(url);
+	if (url != suppressed_rkward_url) RKHTMLWindow::handleRKWardURL(url);
+	suppressed_rkward_url.clear();
+}
+
+void RKWorkplace::suppressRKWardUrlHandling(const QUrl &url) {
+	RK_TRACE(APP);
+	suppressed_rkward_url = url;
 }
 
 bool RKWorkplace::openAnyUrl(const QUrl &url, const QString &known_mimetype, bool force_external) {
diff --git a/rkward/windows/rkworkplace.h b/rkward/windows/rkworkplace.h
index 8cffaa8dd..6884fc3d5 100644
--- a/rkward/windows/rkworkplace.h
+++ b/rkward/windows/rkworkplace.h
@@ -229,6 +229,8 @@ class RKWorkplace : public QWidget {
 	/** Inform the workplace that this window is handled outside the regular attached/detached mechanisms (such as preview windows). Internally, this just sets the window to detached, without giving it a DetachedWindowContainer.
 	This seems good enough for now, but may be something to revisit in case of unexpected problems. */
 	void setWindowNotManaged(RKMDIWindow *window);
+	/** Used from RKQWebView */
+	void suppressRKWardUrlHandling(const QUrl &url);
   Q_SIGNALS:
 	/** emitted when the workspace Url has changed */
 	void workspaceUrlChanged(const QUrl &url);
@@ -284,6 +286,7 @@ class RKWorkplace : public QWidget {
 	RKMDIWindow::State window_placement_override;
 	QString window_name_override;
 	QString window_style_override;
+	QUrl suppressed_rkward_url;
 };
 
 #endif



More information about the rkward-tracker mailing list