[education/rkward] /: Implement callback from qwebview page to rkward, handle selection as proof of concept

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


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

Implement callback from qwebview page to rkward, handle selection as proof of concept

M  +1    -1    CMakeLists.txt
M  +3    -0    rkward/resources.qrc
M  +1    -1    rkward/windows/CMakeLists.txt
M  +86   -7    rkward/windows/rkqwebview.cpp
M  +10   -1    rkward/windows/rkqwebview.h
A  +24   -0    rkward/windows/rkqwebview.js

https://invent.kde.org/education/rkward/-/commit/4e31189741f22071cf089e8683b8f136001433c0

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 327cac8f0..9e4c80a63 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -27,7 +27,7 @@ INCLUDE(ECMAddAppIcon)
 INCLUDE(ECMMarkNonGuiExecutable)
 INCLUDE(FeatureSummary)
 
-FIND_PACKAGE(Qt6 6.6 CONFIG REQUIRED COMPONENTS Widgets Core Xml Network Qml PrintSupport WebEngineWidgets WebView)
+FIND_PACKAGE(Qt6 6.6 CONFIG REQUIRED COMPONENTS Widgets Core Xml Network Qml PrintSupport WebEngineWidgets WebView WebSockets)
 FIND_PACKAGE(KF6 6.0.0 REQUIRED COMPONENTS CoreAddons DocTools I18n XmlGui TextEditor WidgetsAddons Parts Config Notifications WindowSystem Archive BreezeIcons OPTIONAL_COMPONENTS Crash)
 FIND_PACKAGE(Gettext REQUIRED)
 
diff --git a/rkward/resources.qrc b/rkward/resources.qrc
index 19b89ffd0..4d64ad619 100644
--- a/rkward/resources.qrc
+++ b/rkward/resources.qrc
@@ -24,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
 <qresource prefix="/org.kde.syntax-highlighting/syntax-addons">
 	<file alias="rkward.xml">syntax/rkward.xml</file>
 </qresource>
+<qresource prefix="/js">
+	<file alias="rkqwebview.js">windows/rkqwebview.js</file>
+</qresource>
 <qresource prefix="/qml">
 	<file alias="rkqwebview.qml">windows/rkqwebview.qml</file>
 </qresource>
diff --git a/rkward/windows/CMakeLists.txt b/rkward/windows/CMakeLists.txt
index 2192b66a3..fb1f2a169 100644
--- a/rkward/windows/CMakeLists.txt
+++ b/rkward/windows/CMakeLists.txt
@@ -36,4 +36,4 @@ SET(windows_STAT_SRCS
 )
 
 ADD_LIBRARY(windows STATIC ${windows_STAT_SRCS})
-TARGET_LINK_LIBRARIES(windows Qt6::Widgets Qt6::PrintSupport Qt6::WebEngineWidgets Qt6::WebView Qt6::Qml Qt6::Quick Qt6::QuickWidgets KF6::TextEditor KF6::Notifications KF6::WindowSystem KF6::KIOFileWidgets KF6::I18n)
+TARGET_LINK_LIBRARIES(windows Qt6::Widgets Qt6::PrintSupport Qt6::WebEngineWidgets Qt6::WebView Qt6::WebSockets Qt6::Qml Qt6::Quick Qt6::QuickWidgets KF6::TextEditor KF6::Notifications KF6::WindowSystem KF6::KIOFileWidgets KF6::I18n)
diff --git a/rkward/windows/rkqwebview.cpp b/rkward/windows/rkqwebview.cpp
index 208196fc1..0ac24e0a4 100644
--- a/rkward/windows/rkqwebview.cpp
+++ b/rkward/windows/rkqwebview.cpp
@@ -7,9 +7,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
 
 #include "rkqwebview.h"
 
+#include <QFile>
 #include <QMenu>
 #include <QQuickItem>
 #include <QQuickWidget>
+#include <QWebSocket>
+#include <QWebSocketServer>
 
 #include "../misc/rkfindbar.h"
 #include "rkhtmlwindow.h"
@@ -17,8 +20,56 @@ SPDX-License-Identifier: GPL-2.0-or-later
 
 #include "../debug.h"
 
-RKQWebView::RKQWebView(RKHTMLWindow *parent) : RKHTMLViewer(parent), view(nullptr) {
+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]() {
+				auto con = server->nextPendingConnection();
+				const auto url = con->requestUrl().url();
+				auto win = windows.value(url);
+				RK_DEBUG(APP, DL_WARNING, "New connection at %s from window %p", qPrintable(url), win); // TODO
+				if (!win) {
+					con->abort();
+				}
+
+				// TODO handle connection lifetime
+
+				QObject::connect(con, &QWebSocket::textMessageReceived, win, [win](const QString &message) {
+					win->receivedCallbackMessage(message);
+				});
+			});
+		}
+
+		// Window may already be in our map. We need to re-init on every url change
+		auto key = windows.key(window);
+		if (key.isEmpty()) {
+			key = server->serverUrl().url() + u"/"_s + QUuid::createUuid().toString(QUuid::Id128);
+			windows.insert(key, window);
+			QObject::connect(window, &QObject::destroyed, server, [key]() {
+				windows.remove(key);
+			});
+		}
+
+		window->runJS(u"let __rkward = new WebSocket('%1');"_s.arg(key));
+	}
+
+  protected:
+	static QHash<QString, RKQWebView *> windows;
+};
+QHash<QString, RKQWebView *> RKQWebViewCallbackServer::windows;
+
+RKQWebView::RKQWebView(RKHTMLWindow *parent) : RKHTMLViewer(parent), view(nullptr), running_script(false) {
 	RK_TRACE(APP);
+	QFile file(u":/js/rkqwebview.js"_s);
+	if (file.open(QIODevice::ReadOnly)) {
+		installPersistentJS(QString::fromUtf8(file.readAll()), file.fileName());
+	} else {
+		RK_ASSERT(false);
+	}
 }
 
 QUrl RKQWebView::currentAcceptedUrl() const {
@@ -45,6 +96,8 @@ void RKQWebView::onUrlChanged(const QUrl &_url, const QString &error, int status
 void RKQWebView::onLoadFinished(const QUrl &url) {
 	RK_TRACE(APP);
 	RK_ASSERT(currentAcceptedUrl() == url);
+	selected_text.clear();
+	RKQWebViewCallbackServer::initConnection(this);
 	for (const auto &script : std::as_const(persistentScripts)) {
 		RKHTMLViewer::runJS(script);
 	}
@@ -108,15 +161,25 @@ void RKQWebView::installPersistentJS(const QString &script, const QString &id) {
 
 void RKQWebView::runJS(const QString &script, std::function<void(const QVariant &)> callback) {
 	RK_TRACE(APP);
-	RK_ASSERT(runjs_callback == nullptr); // no nested calls, please!
-	runjs_callback = callback;
-	QMetaObject::invokeMethod(webView(), "runJSWrapper", Q_ARG(QString, script));
+	runjs_queue.append({script, callback});
+	tryRunNextScript();
+}
+
+void RKQWebView::tryRunNextScript() {
+	RK_TRACE(APP);
+	if (running_script) return;
+	if (runjs_queue.isEmpty()) return;
+	running_script = true;
+	QMetaObject::invokeMethod(webView(), "runJSWrapper", Q_ARG(QString, runjs_queue.first().script));
 }
 
 void RKQWebView::onRunJSResult(const QVariant &result) {
 	RK_TRACE(APP);
-	if (runjs_callback != nullptr) runjs_callback(result);
-	runjs_callback = std::function<void(const QVariant &)>();
+	RK_ASSERT(!runjs_queue.isEmpty());
+	running_script = false;
+	const auto callback = runjs_queue.takeFirst().callback;
+	if (callback != nullptr) callback(result);
+	tryRunNextScript();
 }
 
 void RKQWebView::setHTML(const QString &html, const QUrl &_url) {
@@ -151,7 +214,23 @@ QMenu *RKQWebView::createContextMenu(const QPoint &clickpos) {
 QString RKQWebView::selectedText() const {
 	RK_TRACE(APP);
 
-	return QString();
+	return selected_text;
+}
+
+void RKQWebView::receivedCallbackMessage(const QString &message) {
+	RK_TRACE(APP);
+	const auto mo = QJsonDocument::fromJson(message.toUtf8()).object();
+	const auto msg = mo["msg"_L1].toString();
+	const auto args = mo["args"_L1];
+	if (msg == "selChanged"_L1) {
+		const auto sel = args["sel"_L1].toString();
+		if (sel != selected_text) {
+			selected_text = sel;
+			Q_EMIT selectionChanged(!sel.isEmpty());
+		}
+	} else {
+		RK_DEBUG(APP, DL_ERROR, "Non-recognized message %s", qPrintable(message));
+	}
 }
 
 void RKQWebView::exportPage() {
diff --git a/rkward/windows/rkqwebview.h b/rkward/windows/rkqwebview.h
index f681105c5..c6b73ada5 100644
--- a/rkward/windows/rkqwebview.h
+++ b/rkward/windows/rkqwebview.h
@@ -41,11 +41,20 @@ class RKQWebView : public RKHTMLViewer {
 
   private:
 	friend class RKHTMLViewer;
+	friend class RKQWebViewCallbackServer;
 	RKQWebView(RKHTMLWindow *parent);
 	QPointer<QQuickWidget> view;
 	QUrl currentAcceptedUrl() const;
 	QMap<QString, QString> persistentScripts;
-	std::function<void(const QVariant &)> runjs_callback;
+	struct Script {
+		QString script;
+		std::function<void(const QVariant &)> callback;
+	};
+	QList<Script> runjs_queue;
+	bool running_script;
+	void tryRunNextScript();
+	void receivedCallbackMessage(const QString &message);
+	QString selected_text;
   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
new file mode 100644
index 000000000..3c9473e6f
--- /dev/null
+++ b/rkward/windows/rkqwebview.js
@@ -0,0 +1,24 @@
+// rkqwebview.js - This file is part of the RKWard project. Created: Sat Mar 14 2026
+// SPDX-FileCopyrightText: 2026 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+// SPDX-FileContributor: The RKWard Team <rkward at kde.org>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+function __rkward_debounce(fun) {
+	let to = null;
+	return function() {
+		clearTimeout(to);
+		to = setTimeout(function() { fun(); }, 20);
+	}
+}
+
+document.onselectionchange = __rkward_debounce(function() {
+	__rkward_sendMessage("selChanged", { sel: document.getSelection().toString() });
+});
+
+function __rkward_sendMessage(msg, args) {
+	if (__rkward.readyState == WebSocket.CONNECTING) {
+		setTimeout(__rkward_sendMessage.bind(null, msg, args), 10);
+	} else {
+		__rkward.send(JSON.stringify({msg, args}));
+	}
+}



More information about the rkward-tracker mailing list