[education/rkward] rkward: Add abstraction layer over html renderer engine to use

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


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

Add abstraction layer over html renderer engine to use

M  +1    -1    rkward/misc/rkfindbar.h
M  +2    -0    rkward/windows/CMakeLists.txt
A  +23   -0    rkward/windows/rkhtmlviewer.cpp     [License: GPL(v2.0+)]
A  +62   -0    rkward/windows/rkhtmlviewer.h     [License: GPL(v2.0+)]
M  +47   -290  rkward/windows/rkhtmlwindow.cpp
M  +3    -10   rkward/windows/rkhtmlwindow.h
M  +0    -1    rkward/windows/rkoutputwindow.rc
A  +379  -0    rkward/windows/rkqwebenginewidget.cpp     [License: GPL(v2.0+)]
A  +46   -0    rkward/windows/rkqwebenginewidget.h     [License: GPL(v2.0+)]

https://invent.kde.org/education/rkward/-/commit/75f01c0ea3585e706e36f49a7f06f94f29ed22bb

diff --git a/rkward/misc/rkfindbar.h b/rkward/misc/rkfindbar.h
index 1f36c707a..e64d4fd35 100644
--- a/rkward/misc/rkfindbar.h
+++ b/rkward/misc/rkfindbar.h
@@ -42,7 +42,7 @@ class RKFindBar : public QWidget {
 	void forward();
 	void backward();
   Q_SIGNALS:
-	void findRequest(const QString &text, bool backwards, const RKFindBar *findbar, bool *result);
+	void findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *result);
   private Q_SLOTS:
 	/** search term _or_ search options changed. Triggers a forward search, if FindAsYouType is active */
 	void searchChanged();
diff --git a/rkward/windows/CMakeLists.txt b/rkward/windows/CMakeLists.txt
index 688206baf..d3bb67b98 100644
--- a/rkward/windows/CMakeLists.txt
+++ b/rkward/windows/CMakeLists.txt
@@ -11,6 +11,8 @@ SET(windows_STAT_SRCS
 	rkdebugconsole.cpp
 	rkcallstackviewer.cpp
 	rkhtmlwindow.cpp
+	rkhtmlviewer.cpp
+	rkqwebenginewidget.cpp
 	rkpdfwindow.cpp
 	rcontrolwindow.cpp
 	detachedwindowcontainer.cpp
diff --git a/rkward/windows/rkhtmlviewer.cpp b/rkward/windows/rkhtmlviewer.cpp
new file mode 100644
index 000000000..058ab113c
--- /dev/null
+++ b/rkward/windows/rkhtmlviewer.cpp
@@ -0,0 +1,23 @@
+/*
+rkhtmlviewerwidget - This file is part of the RKWard project. Created: Sat Feb 07 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
+*/
+
+#include "rkhtmlviewer.h"
+
+#include "rkqwebenginewidget.h"
+
+#include "../debug.h"
+
+RKHTMLViewer::RKHTMLViewer(QObject *parent) : QObject(parent) {
+	RK_TRACE(APP);
+}
+
+RKHTMLViewer *RKHTMLViewer::getNew(RKHTMLWindow *parent) {
+	RK_TRACE(APP);
+	return new RKQWebEngineWidget(parent);
+}
+
+#include "rkhtmlviewer.moc"
diff --git a/rkward/windows/rkhtmlviewer.h b/rkward/windows/rkhtmlviewer.h
new file mode 100644
index 000000000..7bf96a2a2
--- /dev/null
+++ b/rkward/windows/rkhtmlviewer.h
@@ -0,0 +1,62 @@
+/*
+rkhtmlviewerwidget - This file is part of the RKWard project. Created: Sat Feb 07 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
+*/
+
+#ifndef RKHTMLVIEWERWIDGET_H
+#define RKHTMLVIEWERWIDGET_H
+
+#include <QObject>
+#include <QString>
+#include <QUrl>
+
+class QMenu;
+class QWidget;
+class RKFindBar;
+class RKHTMLWindow;
+
+/** Abstract base class for the HTML viewer component */
+class RKHTMLViewer : public QObject {
+	Q_OBJECT
+  public:
+	static RKHTMLViewer *getNew(RKHTMLWindow *parent);
+	virtual QWidget *createWidget(QWidget *parent) = 0;
+	virtual void reload() {
+		load(url());
+	}
+	virtual QUrl url() const = 0;
+	virtual void load(const QUrl &url) = 0;
+	virtual void exportPage() = 0;
+	virtual void print() = 0;
+	virtual void installPersistentJS(const QString &script, const QString &id) = 0;
+	virtual void runJS(const QString &script, std::function<void(const QVariant &)> callback) = 0;
+	void runJS(const QString &script) {
+		runJS(script, [](const QVariant &) {});
+	}
+	virtual void setHTML(const QString &html, const QUrl &url) = 0;
+	virtual bool installHelpProtocolHandler() {
+		return false;
+	}
+	virtual QPoint scrollPosition() const = 0;
+	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;
+	virtual void zoomOut() = 0;
+  Q_SIGNALS:
+	// TODO: code to emit these signals
+	void pageInternalNavigation(const QUrl &new_url);
+	void selectionChanged(bool has_selection);
+	void loadFinished();
+
+  protected:
+	RKHTMLViewer(QObject *parent);
+	RKHTMLWindow *window;
+};
+
+#endif
diff --git a/rkward/windows/rkhtmlwindow.cpp b/rkward/windows/rkhtmlwindow.cpp
index ff99418cf..e1e0cf4e7 100644
--- a/rkward/windows/rkhtmlwindow.cpp
+++ b/rkward/windows/rkhtmlwindow.cpp
@@ -20,9 +20,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
 
 #include <QBuffer>
 #include <QCheckBox>
+#include <QClipboard>
 #include <QDir>
 #include <QFileDialog>
-#include <QFontDatabase>
 #include <QGuiApplication>
 #include <QHBoxLayout>
 #include <QHostInfo>
@@ -30,19 +30,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <QMenu>
 #include <QMimeDatabase>
 #include <QPrintDialog>
-#include <QStringEncoder>
 #include <QTemporaryFile>
 #include <QTimer>
-#include <QWebEngineFindTextResult>
-#include <QWebEnginePage>
-#include <QWebEngineProfile>
-#include <QWebEngineScript>
-#include <QWebEngineScriptCollection>
-#include <QWebEngineSettings>
-#include <QWebEngineUrlRequestJob>
-#include <QWebEngineUrlSchemeHandler>
-#include <QWebEngineView>
-#include <QWheelEvent>
 #include <qfileinfo.h>
 #include <qlayout.h>
 #include <qwidget.h>
@@ -66,6 +55,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include "../settings/rksettings.h"
 #include "../settings/rksettingsmoduleoutput.h"
 #include "../settings/rksettingsmoduler.h"
+#include "../windows/rkhtmlviewer.h"
 #include "../windows/rkworkplace.h"
 #include "../windows/rkworkplaceview.h"
 #include "rkhelpsearchwindow.h"
@@ -74,190 +64,18 @@ QUrl restorableUrl(const QUrl &url) {
 	return QUrl(url.url().replace(RKSettingsModuleR::helpBaseUrl(), QLatin1String("rkward://RHELPBASE")));
 }
 
-class RKWebPage : public QWebEnginePage {
-	Q_OBJECT
-  public:
-	explicit RKWebPage(RKHTMLWindow *window) : QWebEnginePage(window) {
-		RK_TRACE(APP);
-		RKWebPage::window = window;
-		direct_load = false;
-		settings()->setFontFamily(QWebEngineSettings::StandardFont, QFontDatabase::systemFont(QFontDatabase::GeneralFont).family());
-		settings()->setFontFamily(QWebEngineSettings::FixedFont, QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
-	}
-
-	void load(const QUrl &url) {
-		RK_TRACE(APP);
-		direct_load = true;
-		QWebEnginePage::load(url);
-	}
-
-	void setHtmlWrapper(const QString &html, const QUrl &baseurl) {
-		direct_load = true;
-		setHtml(html, baseurl);
-	}
-	bool supportsContentType(const QString &name) {
-		if (name.startsWith(QLatin1String("text"))) return true;
-		return false;
-	}
-	void downloadUrl(const QUrl &url) {
-		download(url);
-	}
-	void setScrollPosition(const QPoint &point) {
-		RK_DEBUG(APP, DL_DEBUG, "scrolling to %d, %d", point.x(), point.y());
-		runJavaScript(QStringLiteral("window.scrollTo(%1, %2);").arg(point.x()).arg(point.y()));
-	}
-	void setScrollPositionWhenDone(const QPoint &pos) {
-		QMetaObject::Connection *const connection = new QMetaObject::Connection;
-		*connection = connect(this, &QWebEnginePage::loadFinished, [this, pos, connection]() {
-			QObject::disconnect(*connection);
-			delete connection;
-			setScrollPosition(pos);
-		});
-	}
-
-  Q_SIGNALS:
-	void pageInternalNavigation(const QUrl &url);
-
-  protected:
-	bool acceptNavigationRequest(const QUrl &navurl, QWebEnginePage::NavigationType type, bool is_main_frame) override {
-		QUrl cururl(url());
-		Q_UNUSED(type);
-
-		RK_TRACE(APP);
-		RK_DEBUG(APP, DL_DEBUG, "Navigation request to %s", qPrintable(navurl.toString()));
-		if (direct_load && (is_main_frame)) {
-			direct_load = false;
-			return true;
-		}
-
-		if (RKHTMLWindow::new_window) {
-			RK_ASSERT(RKHTMLWindow::new_window == this);
-			RK_ASSERT(!window);
-			RKWorkplace::mainWorkplace()->openAnyUrl(restorableUrl(navurl));
-			RKHTMLWindow::new_window = nullptr;
-			if (!window) deleteLater(); // this page was _not_ reused
-			return false;
-		}
-		RK_ASSERT(window);
-
-		if (!is_main_frame) {
-			if (navurl.isLocalFile() && supportsContentType(QMimeDatabase().mimeTypeForUrl(navurl).name())) return true;
-		}
-
-		if (cururl.matches(navurl, QUrl::NormalizePathSegments | QUrl::StripTrailingSlash)) {
-			RK_DEBUG(APP, DL_DEBUG, "Page internal navigation request from %s to %s", qPrintable(cururl.toString()), qPrintable(navurl.toString()));
-			Q_EMIT pageInternalNavigation(navurl);
-			return true;
-		}
-
-		window->openURL(navurl);
-		return false;
-	}
-
-	QWebEnginePage *createWindow(QWebEnginePage::WebWindowType) override {
-		RK_TRACE(APP);
-		RKWebPage *ret = new RKWebPage(nullptr);
-		RKHTMLWindow::new_window = ret; // Don't actually create a full window, until we know which URL we're talking about.
-		// sigh: acceptNavigationRequest() does  not get called on the new page...
-		QMetaObject::Connection *const connection = new QMetaObject::Connection;
-		*connection = connect(ret, &RKWebPage::loadStarted, [ret, connection
-#ifdef _MSC_VER
-		                                                     ,
-		                                                     this // capturing "this" makes MSVC happy
-#endif
-		]() {
-			QObject::disconnect(*connection);
-			delete connection;
-			ret->acceptNavigationRequest(ret->url(), QWebEnginePage::NavigationTypeLinkClicked, true);
-		});
-		return (ret);
-	}
-
-	friend class RKHTMLWindow;
-	RKHTMLWindow *window;
-	bool direct_load;
-};
-
-class RKWebView : public QWebEngineView {
-  public:
-	explicit RKWebView(QWidget *parent) : QWebEngineView(parent) {};
-	void print(QPrinter *printer) {
-		if (!page()) return;
-		QWebEngineView::forPage(page())->print(printer);
-	};
-
-  protected:
-	bool eventFilter(QObject *, QEvent *event) override {
-		if (event->type() == QEvent::Wheel) {
-			QWheelEvent *we = static_cast<QWheelEvent *>(event);
-			if (we->modifiers() & Qt::ControlModifier) {
-				setZoomFactor(zoomFactor() + we->angleDelta().y() / 1200.0);
-				return true;
-			}
-		}
-		return false;
-	}
-	void childEvent(QChildEvent *event) override {
-		if (event->type() == QChildEvent::ChildAdded) {
-			event->child()->installEventFilter(this);
-		}
-	}
-	// NOTE: Code below won't work, due to https://bugreports.qt.io/browse/QTBUG-43602
-	/*	void wheelEvent (QWheelEvent *event) override {
-	        [handle zooming]
-	    } */
-};
-
-class RKWebEngineKIOForwarder : public QWebEngineUrlSchemeHandler {
-  public:
-	explicit RKWebEngineKIOForwarder(QObject *parent) : QWebEngineUrlSchemeHandler(parent) {}
-	void requestStarted(QWebEngineUrlRequestJob *request) override {
-		RK_DEBUG(APP, DL_DEBUG, "new KIO request to %s", qPrintable(request->requestUrl().url()));
-		KIO::StoredTransferJob *job = KIO::storedGet(request->requestUrl(), KIO::NoReload, KIO::HideProgressInfo);
-		connect(job, &KIO::StoredTransferJob::result, this, [this, job]() { kioJobFinished(job); });
-		jobs.insert(job, request);
-	}
-
-  private:
-	void kioJobFinished(KIO::StoredTransferJob *job) {
-		QWebEngineUrlRequestJob *request = jobs.take(job);
-		if (!request) {
-			return;
-		}
-		if (job->error()) {
-			request->fail(QWebEngineUrlRequestJob::UrlInvalid); // TODO
-			return;
-		}
-		QBuffer *buf = new QBuffer(request);
-		buf->setData(job->data());
-		request->reply(QMimeDatabase().mimeTypeForData(job->data()).name().toUtf8(), buf);
-	}
-	QMap<KIO::StoredTransferJob *, QPointer<QWebEngineUrlRequestJob>> jobs;
-};
-
-RKWebPage *RKHTMLWindow::new_window = nullptr;
 RKHTMLWindow::RKHTMLWindow(QWidget *parent, WindowMode mode) : RKMDIWindow(parent, RKMDIWindow::HelpWindow) {
 	RK_TRACE(APP);
 
 	current_cache_file = nullptr;
 	QVBoxLayout *layout = new QVBoxLayout(this);
 	layout->setContentsMargins(0, 0, 0, 0);
-	view = new RKWebView(this);
-	if (new_window) {
-		page = new_window;
-		page->window = this;
-		new_window = nullptr;
-	} else {
-		page = new RKWebPage(this);
-	}
-	view->setPage(page);
+	page = RKHTMLViewer::getNew(this);
+	auto view = page->createWidget(this);
 	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));
-	if (!QWebEngineProfile::defaultProfile()->urlSchemeHandler("help")) {
-		QWebEngineProfile::defaultProfile()->installUrlSchemeHandler("help", new RKWebEngineKIOForwarder(RKWardMainWindow::getMain()));
-	}
 	// Apply current color scheme to page. This needs support in the CSS of the page, so will only work for RKWard help and output pages.
 	// Note that the CSS in those pages also has "automatic" support for dark mode ("prefers-color-scheme: dark"; but see https://bugreports.qt.io/browse/QTBUG-89753), however,
 	// for a seamless appearance, the only option is to set the theme colors dynamically, via javascript.
@@ -271,23 +89,12 @@ 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());
-	auto p = QWebEngineProfile::defaultProfile();
-	QWebEngineScript fix_color_scheme;
-	const QString id = QStringLiteral("fix_color_scheme");
-	const auto scripts = p->scripts()->find(id);
-	for (const auto &script : scripts) {
-		p->scripts()->remove(script); // remove any existing variant of the script. It might have been created for the wrong theme.
-	}
-	fix_color_scheme.setName(id);
-	fix_color_scheme.setInjectionPoint(QWebEngineScript::DocumentReady);
-	fix_color_scheme.setSourceCode(color_scheme_js);
-	p->scripts()->insert(fix_color_scheme);
-	// page->setBackgroundColor(scheme.background().color());  // avoids brief white blink while loading, but is too risky on pages that are not dark scheme aware.
+	page->installPersistentJS(u"fix_color_scheme"_s, color_scheme_js);
+	page->installHelpProtocolHandler();
 
 	layout->addWidget(findbar);
 	findbar->hide();
-	connect(findbar, &RKFindBar::findRequest, this, &RKHTMLWindow::findRequest);
-	have_highlight = false;
+	connect(findbar, &RKFindBar::findRequest, page, &RKHTMLViewer::findRequest);
 
 	part = new RKHTMLWindowPart(this);
 	setPart(part);
@@ -297,19 +104,8 @@ RKHTMLWindow::RKHTMLWindow(QWidget *parent, WindowMode mode) : RKMDIWindow(paren
 	setFocusProxy(view);
 
 	// We have to connect this in order to allow browsing.
-	connect(page, &RKWebPage::pageInternalNavigation, this, &RKHTMLWindow::internalNavigation);
-	connect(page->profile(), &QWebEngineProfile::downloadRequested, this, [this](QWebEngineDownloadRequest *item) {
-		QString defpath;
-		defpath = QDir(item->downloadDirectory()).absoluteFilePath(item->downloadFileName());
-		QString path = QFileDialog::getSaveFileName(this, i18n("Save as"), defpath);
-		if (path.isEmpty()) return;
-		QFileInfo fi(path);
-		item->setDownloadDirectory(fi.absolutePath());
-		item->setDownloadFileName(fi.fileName());
-		item->accept();
-	});
+	connect(page, &RKHTMLViewer::pageInternalNavigation, this, &RKHTMLWindow::internalNavigation);
 
-	connect(page, &RKWebPage::printRequested, this, &RKHTMLWindow::slotPrint);
 	connect(view, &QWidget::customContextMenuRequested, this, &RKHTMLWindow::makeContextMenu);
 
 	current_history_position = -1;
@@ -319,7 +115,7 @@ RKHTMLWindow::RKHTMLWindow(QWidget *parent, WindowMode mode) : RKMDIWindow(paren
 	useMode(mode);
 
 	// needed to enable / disable the run selection action
-	connect(view, &RKWebView::selectionChanged, this, &RKHTMLWindow::selectionChanged);
+	connect(page, &RKHTMLViewer::selectionChanged, this, &RKHTMLWindow::selectionChanged);
 	selectionChanged();
 }
 
@@ -337,13 +133,13 @@ QUrl RKHTMLWindow::restorableUrl() {
 void RKHTMLWindow::makeContextMenu(const QPoint &pos) {
 	RK_TRACE(APP);
 
-	QMenu *menu = QWebEngineView::forPage(page)->createStandardContextMenu();
+	auto menu = page->createContextMenu(pos);
 	menu->addAction(part->run_selection);
-	menu->exec(view->mapToGlobal(pos));
+	menu->exec(mapToGlobal(pos));
 	delete (menu);
 }
 
-void RKHTMLWindow::selectionChanged() {
+void RKHTMLWindow::selectionChanged(bool have_selection) {
 	RK_TRACE(APP);
 
 	if (!(part && part->run_selection)) {
@@ -351,46 +147,24 @@ void RKHTMLWindow::selectionChanged() {
 		return;
 	}
 
-	part->run_selection->setEnabled(view->hasSelection());
+	part->run_selection->setEnabled(have_selection);
 }
 
 void RKHTMLWindow::runSelection() {
 	RK_TRACE(APP);
 
-	RKConsole::pipeUserCommand(view->selectedText());
-}
-
-void RKHTMLWindow::findRequest(const QString &text, bool backwards, const RKFindBar *findbar, bool *found) {
-	RK_TRACE(APP);
-
-	// QWebEngine does not offer highlight all
-	*found = true;
-	QWebEnginePage::FindFlags flags;
-	if (backwards) flags |= QWebEnginePage::FindBackward;
-	if (findbar->isOptionSet(RKFindBar::MatchCase)) flags |= QWebEnginePage::FindCaseSensitively;
-	page->findText(text, flags, [this](QWebEngineFindTextResult result) {
-		if (result.numberOfMatches() == 0) {
-			this->findbar->indicateSearchFail();
-		}
-	});
+	RKConsole::pipeUserCommand(page->selectedText());
 }
 
 void RKHTMLWindow::slotPrint() {
 	RK_TRACE(APP);
-
-	// NOTE: taken from kwebkitpart, with small mods
-	// Make it non-modal, in case a redirection deletes the part
-	QPointer<QPrintDialog> dlg(new QPrintDialog(view));
-	if (dlg->exec() == QPrintDialog::Accepted) {
-		view->print(dlg->printer());
-	}
-	delete dlg;
+	page->print();
 }
 
 void RKHTMLWindow::slotExport() {
 	RK_TRACE(APP);
 
-	page->downloadUrl(page->url());
+	page->exportPage();
 }
 
 void RKHTMLWindow::slotSave() {
@@ -426,11 +200,11 @@ void RKHTMLWindow::openLocationFromHistory(VisitedLocation &loc) {
 	RK_ASSERT(current_history_position <= history_last);
 	QPoint scroll_pos = loc.scroll_position.toPoint();
 	if (loc.url == current_url) {
-		page->setScrollPosition(scroll_pos);
+		page->setScrollPosition(scroll_pos, false);
 	} else {
 		url_change_is_from_history = true;
-		openURL(loc.url); // TODO: merge into restoreBrowserState()?
-		page->setScrollPositionWhenDone(scroll_pos);
+		openURL(loc.url);
+		page->setScrollPosition(scroll_pos, true);
 		url_change_is_from_history = false;
 	}
 
@@ -556,7 +330,7 @@ bool RKHTMLWindow::handleRKWardURL(const QUrl &url, RKHTMLWindow *window) {
 
 void RKHTMLWindow::setContent(const QString &content) {
 	RK_TRACE(APP);
-	page->setHtmlWrapper(content, QUrl());
+	page->setHTML(content, QUrl());
 }
 
 bool RKHTMLWindow::openURL(const QUrl &url) {
@@ -565,7 +339,7 @@ bool RKHTMLWindow::openURL(const QUrl &url) {
 	if (handleRKWardURL(url, this)) return true;
 
 	QPoint restore_position;
-	if (url == current_url) restore_position = page->scrollPosition().toPoint();
+	if (url == current_url) restore_position = page->scrollPosition();
 
 	QMimeType mtype = QMimeDatabase().mimeTypeForUrl(url);
 	if (window_mode == HTMLOutputWindow) {
@@ -595,7 +369,7 @@ bool RKHTMLWindow::openURL(const QUrl &url) {
 				RK_DEBUG(APP, DL_WARNING, "Applying workaround for https://bugs.kde.org/show_bug.cgi?id=405386");
 				QFile f(url.toLocalFile());
 				RK_ASSERT(f.open(QIODevice::ReadOnly));
-				page->setHtmlWrapper(QString::fromUtf8(f.readAll()), url.adjusted(QUrl::RemoveFilename));
+				page->setHTML(QString::fromUtf8(f.readAll()), url.adjusted(QUrl::RemoveFilename));
 				f.close();
 			} else {
 				// NOTE: Quirk in Qt 6.7: When first loading a page, the window is somehow brought to the front, again. In preview windows
@@ -604,11 +378,11 @@ bool RKHTMLWindow::openURL(const QUrl &url) {
 				//       With this, the flicker is still there, but feels more "natural"
 				if (isVisible()) page->load(url);
 			}
-			if (!restore_position.isNull()) page->setScrollPositionWhenDone(restore_position);
+			if (!restore_position.isNull()) page->setScrollPosition(restore_position, true);
 		} else {
 			RK_DEBUG(APP, DL_WARNING, "Attempt to open non-existant local file %s", qPrintable(url.toLocalFile()));
 			if (window_mode == HTMLOutputWindow) {
-				page->setHtmlWrapper(QStringLiteral("<HTML><BODY><H1>") + i18n("RKWard output file %s does not (yet) exist").arg(url.toLocalFile()) + QStringLiteral("</H1>\n</BODY></HTML>"), QUrl());
+				page->setHTML(QStringLiteral("<HTML><BODY><H1>") + i18n("RKWard output file %1 does not (yet) exist").arg(url.toLocalFile()) + QStringLiteral("</H1>\n</BODY></HTML>"), QUrl());
 			} else {
 				fileDoesNotExistMessage();
 			}
@@ -765,8 +539,8 @@ void RKHTMLWindow::refresh() {
 		openRKHPage(url());
 	} else {
 		// NOTE: Special handling for output window, which may not have existed at the time of first "load"
-		if (!view->url().isEmpty()) view->reload();
-		else view->load(url());
+		if (!page->url().isEmpty()) page->reload();
+		else page->load(url());
 	}
 }
 
@@ -774,25 +548,17 @@ void RKHTMLWindow::scrollToBottom() {
 	RK_TRACE(APP);
 
 	RK_ASSERT(window_mode == HTMLOutputWindow);
-	page->runJavaScript(QStringLiteral("{ let se = (document.scrollingElement || document.body); se.scrollTop = se.scrollHeight; }"));
+	page->runJS(QStringLiteral("{ let se = (document.scrollingElement || document.body); se.scrollTop = se.scrollHeight; }"));
 }
 
 void RKHTMLWindow::zoomIn() {
 	RK_TRACE(APP);
-	view->setZoomFactor(view->zoomFactor() * 1.1);
+	page->zoomIn();
 }
 
 void RKHTMLWindow::zoomOut() {
 	RK_TRACE(APP);
-	view->setZoomFactor(view->zoomFactor() / 1.1);
-}
-
-void RKHTMLWindow::setTextEncoding(QStringConverter::Encoding encoding) {
-	RK_TRACE(APP);
-
-	QStringEncoder converter(encoding);
-	page->settings()->setDefaultTextEncoding(QString::fromUtf8(converter.name()));
-	view->reload();
+	page->zoomOut();
 }
 
 void RKHTMLWindow::useMode(WindowMode new_mode) {
@@ -805,8 +571,8 @@ void RKHTMLWindow::useMode(WindowMode new_mode) {
 		setWindowIcon(RKStandardIcons::getIcon(RKStandardIcons::WindowOutput));
 		part->setOutputWindowSkin();
 		setMetaInfo(i18n("Output Window"), QUrl(QStringLiteral("rkward://page/rkward_output")), RKSettingsModuleOutput::page_id);
-		connect(page, &RKWebPage::loadFinished, this, &RKHTMLWindow::scrollToBottom);
-		page->action(RKWebPage::Reload)->setText(i18n("&Refresh Output"));
+		connect(page, &RKHTMLViewer::loadFinished, this, &RKHTMLWindow::scrollToBottom);
+		part->outputRefresh->setText(i18n("&Refresh Output"));
 
 		//	TODO: This would be an interesting extension, but how to deal with concurrent edits?
 		//		page->setContentEditable (true);
@@ -816,7 +582,7 @@ void RKHTMLWindow::useMode(WindowMode new_mode) {
 		type = RKMDIWindow::HelpWindow | RKMDIWindow::DocumentWindow;
 		setWindowIcon(RKStandardIcons::getIcon(RKStandardIcons::WindowHelp));
 		part->setHelpWindowSkin();
-		disconnect(page, &RKWebPage::loadFinished, this, &RKHTMLWindow::scrollToBottom);
+		disconnect(page, &RKHTMLViewer::loadFinished, this, &RKHTMLWindow::scrollToBottom);
 	}
 
 	updateCaption(current_url);
@@ -832,7 +598,7 @@ void RKHTMLWindow::startNewCacheFile() {
 void RKHTMLWindow::fileDoesNotExistMessage() {
 	RK_TRACE(APP);
 
-	page->setHtmlWrapper(u"<html><body><h1>"_s + i18n("Page does not exist or is broken") + u"</h1></body></html>"_s, QUrl());
+	page->setHTML(u"<html><body><h1>"_s + i18n("Page does not exist or is broken") + u"</h1></body></html>"_s, QUrl());
 }
 
 void RKHTMLWindow::flushOutput() {
@@ -878,34 +644,23 @@ RKHTMLWindowPart::RKHTMLWindowPart(RKHTMLWindow *window) : KParts::Part(window)
 void RKHTMLWindowPart::initActions() {
 	RK_TRACE(APP);
 
-	// We keep our own history.
-	window->page->action(RKWebPage::Back)->setVisible(false);
-	window->page->action(RKWebPage::Forward)->setVisible(false);
-	// For now we won't bother with this one: Does not behave well, in particular (but not only) WRT to rkward://-links
-	window->page->action(RKWebPage::DownloadLinkToDisk)->setVisible(false);
-	// Not really useful for us, and cannot easily be made to work, as all new pages go through RKWorkplace::openAnyUrl()
-	window->page->action(RKWebPage::ViewSource)->setVisible(false);
-	// Well, technically, all our windows are tabs, but we're calling them "window".
-	// At any rate, we don't need both "open link in new tab" and "open link in new window".
-	window->page->action(RKWebPage::OpenLinkInNewTab)->setVisible(false);
-
 	// common actions
-	actionCollection()->addAction(KStandardAction::Copy, QStringLiteral("copy"), window->view->pageAction(RKWebPage::Copy), &QAction::trigger);
+	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);
+			}
+		});
+	});
 	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));
 	connect(zoom_out, &QAction::triggered, window, &RKHTMLWindow::zoomOut);
-	actionCollection()->addAction(KStandardAction::SelectAll, QStringLiteral("select_all"), window->view->pageAction(RKWebPage::SelectAll), &QAction::trigger);
-	// unfortunately, this will only affect the default encoding, not necessarily the "real" encoding
-	KCodecAction *encoding = new KCodecAction(QIcon::fromTheme(QStringLiteral("character-set")), i18n("Default &Encoding"), this, true);
-	encoding->setWhatsThis(i18n("Set the encoding to assume in case no explicit encoding has been set in the page or in the HTTP headers."));
-	actionCollection()->addAction(QStringLiteral("view_encoding"), encoding);
-	connect(encoding, &KCodecAction::codecNameTriggered, window, [this](const QByteArray &name) {
-		auto encoding = QStringConverter::encodingForName(name.constData());
-		if (encoding) {
-			window->setTextEncoding(encoding.value());
-		}
+	actionCollection()->addAction(KStandardAction::SelectAll, QStringLiteral("select_all"), window->page, [page = window->page]() {
+		page->runJS(u"{ Document.selectAll(document); }"_s);
 	});
+	// TODO: We used to have an encoding action. Did not seem worth the trouble to port
 
 	print = actionCollection()->addAction(KStandardAction::Print, QStringLiteral("print_html"), window, &RKHTMLWindow::slotPrint);
 	export_page = actionCollection()->addAction(QStringLiteral("save_html"), new QAction(QIcon::fromTheme(QStringLiteral("file-save")), i18n("Export Page as HTML"), this));
@@ -930,7 +685,9 @@ void RKHTMLWindowPart::initActions() {
 	outputFlush->setText(i18n("&Clear Output"));
 	outputFlush->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
 
-	outputRefresh = actionCollection()->addAction(QStringLiteral("output_refresh"), window->page->action(RKWebPage::Reload));
+	outputRefresh = actionCollection()->addAction(QStringLiteral("output_refresh"), window->page, &RKHTMLViewer::reload);
+	outputRefresh->setText(i18n("Reload"));
+	outputRefresh->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
 
 	revert = actionCollection()->addAction(QStringLiteral("output_revert"), window, &RKHTMLWindow::slotRevert);
 	revert->setText(i18n("&Revert to last saved state"));
diff --git a/rkward/windows/rkhtmlwindow.h b/rkward/windows/rkhtmlwindow.h
index 54ca96951..51f3edcd6 100644
--- a/rkward/windows/rkhtmlwindow.h
+++ b/rkward/windows/rkhtmlwindow.h
@@ -30,8 +30,7 @@ class QTemporaryFile;
 class RKHTMLWindow;
 class RKFindBar;
 class RCommandChain;
-class RKWebPage;
-class RKWebView;
+class RKHTMLViewer;
 class RKOutputDirectory;
 
 /**
@@ -84,7 +83,7 @@ class RKHTMLWindow : public RKMDIWindow {
 	void slotActivate();
 	void slotForward();
 	void slotBack();
-	void selectionChanged();
+	void selectionChanged(bool have_selection = false);
 	void runSelection();
 	/** flush current output. */
 	void flushOutput();
@@ -92,7 +91,6 @@ class RKHTMLWindow : public RKMDIWindow {
 	void refresh();
 	void zoomIn();
 	void zoomOut();
-	void setTextEncoding(QStringConverter::Encoding encoding);
 	void updateState();
   private Q_SLOTS:
 	void scrollToBottom();
@@ -101,14 +99,11 @@ class RKHTMLWindow : public RKMDIWindow {
 	void mimeTypeJobFail2(KJob *);
 	void internalNavigation(const QUrl &new_url);
 	void makeContextMenu(const QPoint &pos);
-	void findRequest(const QString &text, bool backwards, const RKFindBar *findbar, bool *found);
 
   private:
 	friend class RKHTMLWindowPart;
-	RKWebView *view;
-	RKWebPage *page;
+	RKHTMLViewer *page;
 	RKFindBar *findbar;
-	bool have_highlight;
 	/** In case the part is a khtmlpart: A ready-cast pointer to that. 0 otherwise (if a webkit part is in use) */
 	RKHTMLWindowPart *part;
 	/** update caption according to given URL */
@@ -138,8 +133,6 @@ class RKHTMLWindow : public RKMDIWindow {
 	void saveBrowserState(VisitedLocation *state);
 	/** the RKOutpuDirectory viewed in this window (if any) */
 	RKOutputDirectory *dir;
-	friend class RKWebPage;
-	static RKWebPage *new_window;
 };
 
 class RKHTMLWindowPart : public KParts::Part {
diff --git a/rkward/windows/rkoutputwindow.rc b/rkward/windows/rkoutputwindow.rc
index 124888cbd..ce355acd8 100644
--- a/rkward/windows/rkoutputwindow.rc
+++ b/rkward/windows/rkoutputwindow.rc
@@ -28,7 +28,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
 		<Menu name="view"><text>&View</text>
 			<Action name="zoom_in"/>
 			<Action name="zoom_out"/>
-			<Action name="view_encoding"/>
 			<Separator/>
 			<Action name="output_refresh"/>
 		</Menu>
diff --git a/rkward/windows/rkqwebenginewidget.cpp b/rkward/windows/rkqwebenginewidget.cpp
new file mode 100644
index 000000000..29c2795a8
--- /dev/null
+++ b/rkward/windows/rkqwebenginewidget.cpp
@@ -0,0 +1,379 @@
+/*
+rkqwebenginewidget - This file is part of the RKWard project. Created: Sat Feb 07 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
+*/
+
+#include "rkqwebenginewidget.h"
+
+#include <QBuffer>
+#include <QDir>
+#include <QFileDialog>
+#include <QFontDatabase>
+#include <QMimeDatabase>
+#include <QPrintDialog>
+#include <QWebEngineFindTextResult>
+#include <QWebEnginePage>
+#include <QWebEngineProfile>
+#include <QWebEngineScript>
+#include <QWebEngineScriptCollection>
+#include <QWebEngineSettings>
+#include <QWebEngineUrlRequestJob>
+#include <QWebEngineUrlSchemeHandler>
+#include <QWebEngineView>
+#include <QWheelEvent>
+
+#include <KIO/StoredTransferJob>
+#include <KLocalizedString>
+
+#include "../misc/rkfindbar.h"
+#include "../rkward.h"
+#include "../settings/rksettingsmoduler.h"
+#include "rkhtmlwindow.h"
+#include "rkworkplace.h"
+
+#include "../debug.h"
+
+// TODO: remove dupe
+static QUrl restorableUrl(const QUrl &url) {
+	return QUrl(url.url().replace(RKSettingsModuleR::helpBaseUrl(), QLatin1String("rkward://RHELPBASE")));
+}
+
+class RKWebPage : public QWebEnginePage {
+	Q_OBJECT
+  public:
+	explicit RKWebPage(RKHTMLWindow *window) : QWebEnginePage(window) {
+		RK_TRACE(APP);
+		RKWebPage::window = window;
+		direct_load = false;
+		settings()->setFontFamily(QWebEngineSettings::StandardFont, QFontDatabase::systemFont(QFontDatabase::GeneralFont).family());
+		settings()->setFontFamily(QWebEngineSettings::FixedFont, QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
+	}
+
+	void load(const QUrl &url) {
+		RK_TRACE(APP);
+		direct_load = true;
+		QWebEnginePage::load(url);
+	}
+
+	void setHtmlWrapper(const QString &html, const QUrl &baseurl) {
+		direct_load = true;
+		setHtml(html, baseurl);
+	}
+	bool supportsContentType(const QString &name) {
+		if (name.startsWith(QLatin1String("text"))) return true;
+		return false;
+	}
+	void downloadUrl(const QUrl &url) {
+		download(url);
+	}
+	void setScrollPosition(const QPoint &point) {
+		RK_DEBUG(APP, DL_DEBUG, "scrolling to %d, %d", point.x(), point.y());
+		runJavaScript(QStringLiteral("window.scrollTo(%1, %2);").arg(point.x()).arg(point.y()));
+	}
+	void setScrollPositionWhenDone(const QPoint &pos) {
+		QMetaObject::Connection *const connection = new QMetaObject::Connection;
+		*connection = connect(this, &QWebEnginePage::loadFinished, [this, pos, connection]() {
+			QObject::disconnect(*connection);
+			delete connection;
+			setScrollPosition(pos);
+		});
+	}
+
+  Q_SIGNALS:
+	void pageInternalNavigation(const QUrl &url);
+
+  protected:
+	bool acceptNavigationRequest(const QUrl &navurl, QWebEnginePage::NavigationType type, bool is_main_frame) override {
+		QUrl cururl(url());
+		Q_UNUSED(type);
+
+		RK_TRACE(APP);
+		RK_DEBUG(APP, DL_DEBUG, "Navigation request to %s", qPrintable(navurl.toString()));
+		if (direct_load && (is_main_frame)) {
+			direct_load = false;
+			return true;
+		}
+
+		if (opened_as_new) {
+			RK_ASSERT(opened_as_new == this);
+			RK_ASSERT(!window);
+			RKWorkplace::mainWorkplace()->openAnyUrl(restorableUrl(navurl));
+			opened_as_new = nullptr;
+			if (!window) deleteLater(); // this page was _not_ reused
+			return false;
+		}
+		RK_ASSERT(window);
+
+		if (!is_main_frame) {
+			if (navurl.isLocalFile() && supportsContentType(QMimeDatabase().mimeTypeForUrl(navurl).name())) return true;
+		}
+
+		if (cururl.matches(navurl, QUrl::NormalizePathSegments | QUrl::StripTrailingSlash)) {
+			RK_DEBUG(APP, DL_DEBUG, "Page internal navigation request from %s to %s", qPrintable(cururl.toString()), qPrintable(navurl.toString()));
+			Q_EMIT pageInternalNavigation(navurl);
+			return true;
+		}
+
+		window->openURL(navurl);
+		return false;
+	}
+
+	QWebEnginePage *createWindow(QWebEnginePage::WebWindowType) override {
+		RK_TRACE(APP);
+		RKWebPage *ret = new RKWebPage(nullptr);
+		opened_as_new = ret; // Don't actually create a full window, until we know which URL we're talking about.
+		// sigh: acceptNavigationRequest() does  not get called on the new page...
+		QMetaObject::Connection *const connection = new QMetaObject::Connection;
+		*connection = connect(ret, &RKWebPage::loadStarted, [ret, connection
+#ifdef _MSC_VER
+		                                                     ,
+		                                                     this // capturing "this" makes MSVC happy
+#endif
+		]() {
+			QObject::disconnect(*connection);
+			delete connection;
+			ret->acceptNavigationRequest(ret->url(), QWebEnginePage::NavigationTypeLinkClicked, true);
+		});
+		return (ret);
+	}
+
+	friend class RKQWebEngineWidget;
+	RKHTMLWindow *window;
+	bool direct_load;
+	static RKWebPage *opened_as_new;
+};
+RKWebPage *RKWebPage::opened_as_new = nullptr;
+
+class RKWebView : public QWebEngineView {
+  public:
+	explicit RKWebView(QWidget *parent) : QWebEngineView(parent) {};
+	void print(QPrinter *printer) {
+		if (!page()) return;
+		QWebEngineView::forPage(page())->print(printer);
+	};
+
+  protected:
+	bool eventFilter(QObject *, QEvent *event) override {
+		if (event->type() == QEvent::Wheel) {
+			QWheelEvent *we = static_cast<QWheelEvent *>(event);
+			if (we->modifiers() & Qt::ControlModifier) {
+				setZoomFactor(zoomFactor() + we->angleDelta().y() / 1200.0);
+				return true;
+			}
+		}
+		return false;
+	}
+	void childEvent(QChildEvent *event) override {
+		if (event->type() == QChildEvent::ChildAdded) {
+			event->child()->installEventFilter(this);
+		}
+	}
+	// NOTE: Code below won't work, due to https://bugreports.qt.io/browse/QTBUG-43602
+	/*	void wheelEvent (QWheelEvent *event) override {
+	        [handle zooming]
+	    } */
+};
+
+class RKWebEngineKIOForwarder : public QWebEngineUrlSchemeHandler {
+  public:
+	explicit RKWebEngineKIOForwarder(QObject *parent) : QWebEngineUrlSchemeHandler(parent) {}
+	void requestStarted(QWebEngineUrlRequestJob *request) override {
+		RK_DEBUG(APP, DL_DEBUG, "new KIO request to %s", qPrintable(request->requestUrl().url()));
+		KIO::StoredTransferJob *job = KIO::storedGet(request->requestUrl(), KIO::NoReload, KIO::HideProgressInfo);
+		connect(job, &KIO::StoredTransferJob::result, this, [this, job]() { kioJobFinished(job); });
+		jobs.insert(job, request);
+	}
+
+  private:
+	void kioJobFinished(KIO::StoredTransferJob *job) {
+		QWebEngineUrlRequestJob *request = jobs.take(job);
+		if (!request) {
+			return;
+		}
+		if (job->error()) {
+			request->fail(QWebEngineUrlRequestJob::UrlInvalid); // TODO
+			return;
+		}
+		QBuffer *buf = new QBuffer(request);
+		buf->setData(job->data());
+		request->reply(QMimeDatabase().mimeTypeForData(job->data()).name().toUtf8(), buf);
+	}
+	QMap<KIO::StoredTransferJob *, QPointer<QWebEngineUrlRequestJob>> jobs;
+};
+
+RKQWebEngineWidget::RKQWebEngineWidget(RKHTMLWindow *parent) : RKHTMLViewer(parent) {
+	RK_TRACE(APP);
+
+	if (RKWebPage::opened_as_new) {
+		page = RKWebPage::opened_as_new;
+		RKWebPage::opened_as_new = nullptr;
+		RKWebPage::opened_as_new->window = parent;
+	} else {
+		page = new RKWebPage(parent);
+	}
+}
+
+QWidget *RKQWebEngineWidget::createWidget(QWidget *parent) {
+	RK_TRACE(APP);
+
+	// We keep our own history.
+	page->action(RKWebPage::Back)->setVisible(false);
+	page->action(RKWebPage::Forward)->setVisible(false);
+	// For now we won't bother with this one: Does not behave well, in particular (but not only) WRT to rkward://-links
+	page->action(RKWebPage::DownloadLinkToDisk)->setVisible(false);
+	// Not really useful for us, and cannot easily be made to work, as all new pages go through RKWorkplace::openAnyUrl()
+	page->action(RKWebPage::ViewSource)->setVisible(false);
+	// Well, technically, all our windows are tabs, but we're calling them "window".
+	// At any rate, we don't need both "open link in new tab" and "open link in new window".
+	page->action(RKWebPage::OpenLinkInNewTab)->setVisible(false);
+
+	connect(page, &RKWebPage::printRequested, this, &RKQWebEngineWidget::print);
+	connect(page->profile(), &QWebEngineProfile::downloadRequested, this, [this](QWebEngineDownloadRequest *item) {
+		QString defpath;
+		defpath = QDir(item->downloadDirectory()).absoluteFilePath(item->downloadFileName());
+		QString path = QFileDialog::getSaveFileName(window, i18n("Save as"), defpath);
+		if (path.isEmpty()) return;
+		QFileInfo fi(path);
+		item->setDownloadDirectory(fi.absolutePath());
+		item->setDownloadFileName(fi.fileName());
+		item->accept();
+	});
+	connect(page, &RKWebPage::loadFinished, this, [this]() {
+		Q_EMIT loadFinished();
+	});
+
+	RK_ASSERT(!view);
+	view = new QWebEngineView(page, parent);
+	connect(view, &QWebEngineView::selectionChanged, this, [this]() {
+		Q_EMIT selectionChanged(view->hasSelection());
+	});
+	return view;
+}
+
+void RKQWebEngineWidget::reload() {
+	RK_TRACE(APP);
+	auto a = page->action(RKWebPage::Reload);
+	RK_ASSERT(a);
+	if (a) a->trigger();
+}
+
+QUrl RKQWebEngineWidget::url() const {
+	RK_TRACE(APP);
+	return page->url();
+}
+
+void RKQWebEngineWidget::load(const QUrl &url) {
+	RK_TRACE(APP);
+	// TODO: We seem to be loading start page multiple times?
+	RK_DEBUG(APP, DL_WARNING, qPrintable(url.url()));
+	page->load(url);
+}
+
+QString RKQWebEngineWidget::selectedText() const {
+	RK_TRACE(APP);
+	if (!view) return QString();
+	return view->selectedText();
+}
+
+void RKQWebEngineWidget::exportPage() {
+	RK_TRACE(APP);
+	page->downloadUrl(page->url());
+}
+
+QPoint RKQWebEngineWidget::scrollPosition() const {
+	RK_TRACE(APP);
+	return page->scrollPosition().toPoint();
+}
+
+void RKQWebEngineWidget::setScrollPosition(const QPoint &pos, bool wait_for_load) {
+	RK_TRACE(APP);
+	if (wait_for_load) {
+		page->setScrollPositionWhenDone(pos);
+	} else {
+		page->setScrollPosition(pos);
+	}
+}
+
+void RKQWebEngineWidget::zoomIn() {
+	RK_TRACE(APP);
+	if (view) view->setZoomFactor(view->zoomFactor() * 1.1);
+}
+
+void RKQWebEngineWidget::zoomOut() {
+	RK_TRACE(APP);
+	if (view) view->setZoomFactor(view->zoomFactor() / 1.1);
+}
+
+bool RKQWebEngineWidget::supportsContentType(const QString &mimename) {
+	RK_TRACE(APP);
+	return page->supportsContentType(mimename);
+}
+
+void RKQWebEngineWidget::print() {
+	RK_TRACE(APP);
+	// NOTE: taken from kwebkitpart, with small mods
+	// Make it non-modal, in case a redirection deletes the part
+	QPointer<QPrintDialog> dlg(new QPrintDialog(view));
+	if (dlg->exec() == QPrintDialog::Accepted) {
+		view->print(dlg->printer());
+	}
+	delete dlg;
+}
+
+void RKQWebEngineWidget::installPersistentJS(const QString &script, const QString &id) {
+	RK_TRACE(APP);
+
+	auto p = QWebEngineProfile::defaultProfile();
+	QWebEngineScript we_script;
+	const auto scripts = p->scripts()->find(id);
+	for (const auto &script : scripts) {
+		p->scripts()->remove(script); // remove any existing variant of the script. It might have been created for the wrong theme.
+	}
+	we_script.setName(id);
+	we_script.setInjectionPoint(QWebEngineScript::DocumentReady);
+	we_script.setSourceCode(script);
+	p->scripts()->insert(we_script);
+}
+
+void RKQWebEngineWidget::runJS(const QString &script, std::function<void(const QVariant &)> callback) {
+	RK_TRACE(APP);
+	page->runJavaScript(script, callback);
+}
+
+void RKQWebEngineWidget::setHTML(const QString &html, const QUrl &url) {
+	RK_TRACE(APP);
+	page->setHtmlWrapper(html, url);
+}
+
+bool RKQWebEngineWidget::installHelpProtocolHandler() {
+	RK_TRACE(APP);
+
+	if (!QWebEngineProfile::defaultProfile()->urlSchemeHandler("help")) {
+		QWebEngineProfile::defaultProfile()->installUrlSchemeHandler("help", new RKWebEngineKIOForwarder(RKWardMainWindow::getMain()));
+	}
+	return true;
+}
+
+void RKQWebEngineWidget::findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *found) {
+	RK_TRACE(APP);
+
+	// QWebEngine does not offer highlight all
+	*found = true; // real result is not available, synchronously
+	QWebEnginePage::FindFlags flags;
+	if (backwards) flags |= QWebEnginePage::FindBackward;
+	if (findbar->isOptionSet(RKFindBar::MatchCase)) flags |= QWebEnginePage::FindCaseSensitively;
+	page->findText(text, flags, [this, findbar](QWebEngineFindTextResult result) {
+		if (result.numberOfMatches() == 0) {
+			findbar->indicateSearchFail();
+		}
+	});
+}
+
+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
new file mode 100644
index 000000000..297eba049
--- /dev/null
+++ b/rkward/windows/rkqwebenginewidget.h
@@ -0,0 +1,46 @@
+/*
+rkqwebenginewidget - This file is part of the RKWard project. Created: Sat Feb 07 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
+*/
+
+#ifndef RKQWEBENGINEWIDGET_H
+#define RKQWEBENGINEWIDGET_H
+
+#include <QPointer>
+
+#include "rkhtmlviewer.h"
+
+class RKWebPage;
+class QWebEngineView;
+
+class RKQWebEngineWidget : public RKHTMLViewer {
+  public:
+	QWidget *createWidget(QWidget *parent) override;
+	void reload() override;
+	QUrl url() const override;
+	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 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;
+	void setScrollPosition(const QPoint &pos, bool wait_for_load) override;
+	bool supportsContentType(const QString &mimename) override;
+	void zoomIn() override;
+	void zoomOut() override;
+
+  private:
+	friend class RKHTMLViewer;
+	RKQWebEngineWidget(RKHTMLWindow *parent);
+	RKWebPage *page;
+	QPointer<QWebEngineView> view;
+};
+
+#endif



More information about the rkward-tracker mailing list