| | | 1 | | // SPDX-FileCopyrightText: 2026 Alper Çelik |
| | | 2 | | // SPDX-License-Identifier: MIT OR Apache-2.0 |
| | | 3 | | |
| | | 4 | | // Qt-backed webview registry (see qt_webview.hpp for the threading contract: |
| | | 5 | | // every entry point here runs on the Qt GUI thread). |
| | | 6 | | // |
| | | 7 | | // The page-event callback `qt_on_webview_event` comes from the cxx bridge. |
| | | 8 | | |
| | | 9 | | #include "qt_webview.hpp" |
| | | 10 | | |
| | | 11 | | #include "qt_scheme.hpp" |
| | | 12 | | #include "qt_window.hpp" |
| | | 13 | | #include "tauri-runtime-qt/src/qt.rs.h" |
| | | 14 | | #include "tauri-runtime-qt/src/bridge/webview.rs.h" |
| | | 15 | | |
| | | 16 | | #include <QtCore/QByteArray> |
| | | 17 | | #include <QtCore/QJsonArray> |
| | | 18 | | #include <QtCore/QJsonDocument> |
| | | 19 | | #include <QtCore/QJsonValue> |
| | | 20 | | #include <QtCore/QObject> |
| | | 21 | | #include <QtCore/QString> |
| | | 22 | | #include <QtCore/QUrl> |
| | | 23 | | #include <QtCore/QVariant> |
| | | 24 | | #include <QtGui/QColor> |
| | | 25 | | #include <QtPrintSupport/QPrintDialog> |
| | | 26 | | #include <QtPrintSupport/QPrinter> |
| | | 27 | | #include <QtWebEngineCore/QWebEngineCookieStore> |
| | | 28 | | #include <QtWebEngineCore/QWebEnginePage> |
| | | 29 | | #include <QtWebEngineCore/QWebEngineProfile> |
| | | 30 | | #include <QtWebEngineCore/QWebEngineScript> |
| | | 31 | | #include <QtWebEngineCore/QWebEngineScriptCollection> |
| | | 32 | | #include <QtWebEngineWidgets/QWebEngineView> |
| | | 33 | | #include <QtWidgets/QDialog> |
| | | 34 | | #include <QtWidgets/QLayout> |
| | | 35 | | |
| | | 36 | | #include <cmath> |
| | | 37 | | #include <cstddef> |
| | | 38 | | #include <unordered_map> |
| | | 39 | | |
| | | 40 | | namespace { |
| | | 41 | | |
| | | 42 | | struct Entry { |
| | | 43 | | QWebEngineView* view = nullptr; |
| | | 44 | | QWebEngineView* devtools_view = nullptr; |
| | | 45 | | QWebEnginePage* devtools_page = nullptr; |
| | | 46 | | }; |
| | | 47 | | |
| | | 48 | | std::unordered_map<uint64_t, Entry> g_webviews; |
| | | 49 | | uint64_t g_next_id = 1; |
| | | 50 | | uint64_t g_next_script_id = 1; |
| | | 51 | | |
| | 280 | 52 | | Entry* find_entry(uint64_t id) { |
| | 280 | 53 | | auto it = g_webviews.find(id); |
| | 280 | 54 | | return it == g_webviews.end() ? nullptr : &it->second; |
| | 280 | 55 | | } |
| | | 56 | | |
| | 200 | 57 | | QString to_qstring(rust::Str s) { |
| | 200 | 58 | | return QString::fromUtf8(s.data(), static_cast<qsizetype>(s.size())); |
| | 200 | 59 | | } |
| | | 60 | | |
| | | 61 | | /// Serializes a `runJavaScript` completion value to compact JSON text. |
| | | 62 | | /// |
| | | 63 | | /// `QJsonDocument::fromVariant()` alone is not enough: it converts only JSON |
| | | 64 | | /// *containers* (`QVariantMap`/`QVariantHash`/`QVariantList`/`QStringList`) and |
| | | 65 | | /// returns a **null document** for every scalar (`QString`, `int`, `bool`, ...; |
| | | 66 | | /// verified against Qt 6.11), which would make every scalar result look like |
| | | 67 | | /// `null`. So the value is wrapped in a one-element array and the brackets are |
| | | 68 | | /// stripped again: a compact JSON array is exactly `[<value>]` with no |
| | | 69 | | /// whitespace, and a value can never contain an unbalanced top-level bracket |
| | | 70 | | /// (JSON escapes them inside strings). This keeps Qt's own escaping. |
| | | 71 | | /// |
| | | 72 | | /// JS `undefined` (or anything Qt cannot represent as a `QJsonValue`) is |
| | | 73 | | /// reported as the JSON literal `null`, matching what JSON can express. |
| | 0 | 74 | | QByteArray json_text(const QVariant& value) { |
| | 0 | 75 | | const QJsonValue json_value = QJsonValue::fromVariant(value); |
| | 0 | 76 | | if (json_value.isUndefined()) { |
| | 0 | 77 | | return QByteArrayLiteral("null"); |
| | 0 | 78 | | } |
| | 0 | 79 | | const QJsonDocument document(QJsonArray{json_value}); |
| | 0 | 80 | | const QByteArray wrapped = document.toJson(QJsonDocument::Compact); |
| | 0 | 81 | | return wrapped.mid(1, wrapped.size() - 2); |
| | 0 | 82 | | } |
| | | 83 | | |
| | | 84 | | /// Emits a page event to Rust. |
| | | 85 | | /// Kinds (must match the bridge documentation in `qt.rs`): |
| | | 86 | | /// 1 = page started loading (url) |
| | | 87 | | /// 2 = page finished loading (url) -- emitted for success and failure |
| | | 88 | | /// 3 = url changed (url) |
| | | 89 | | /// 4 = title changed (title) |
| | | 90 | | /// 5 = load failed (url) -- additionally emitted when loadFinished(false) |
| | 120 | 91 | | void emit_event(uint64_t id, int kind, const QString& payload) { |
| | 120 | 92 | | const QByteArray utf8 = payload.toUtf8(); |
| | 120 | 93 | | qt_on_webview_event(id, kind, |
| | 120 | 94 | | rust::Str(utf8.constData(), static_cast<std::size_t>(utf8.size()))); |
| | 120 | 95 | | } |
| | | 96 | | |
| | | 97 | | /// Takes `view` out of its parent's layout, if it is managed by one. |
| | | 98 | | /// |
| | | 99 | | /// GEOMETRY OWNERSHIP (the decision the runtime makes): the window agent's |
| | | 100 | | /// `qt_window_attach_webview()` puts every attached view into the window's |
| | | 101 | | /// zero-margin `QVBoxLayout`, which makes the webview fill the window client |
| | | 102 | | /// area and follow every window resize — the right default for tauri, where a |
| | | 103 | | /// window's webview *is* its client area. `set_bounds` / `set_size` / |
| | | 104 | | /// `set_position`, in contrast, mean "place the webview exactly here", which a |
| | | 105 | | /// layout cannot express (it would immediately re-apply its own geometry), so |
| | | 106 | | /// the first such call detaches the view from the layout and Qt stops managing |
| | | 107 | | /// its geometry. Consequence, documented in `ops/webview.rs`: from then on the |
| | | 108 | | /// webview no longer resizes with its window (tauri's `set_auto_resize` is a |
| | | 109 | | /// no-op on this runtime), and re-attaching the view to a window |
| | | 110 | | /// (`qt_webview_reparent`) puts it back under layout management. |
| | 0 | 111 | | void detach_from_layout(QWebEngineView* view) { |
| | 0 | 112 | | QWidget* parent = view->parentWidget(); |
| | 0 | 113 | | if (parent == nullptr) { |
| | 0 | 114 | | return; |
| | 0 | 115 | | } |
| | 0 | 116 | | if (QLayout* layout = parent->layout()) { |
| | 0 | 117 | | layout->removeWidget(view); |
| | 0 | 118 | | } |
| | 0 | 119 | | } |
| | | 120 | | |
| | | 121 | | } // namespace |
| | | 122 | | |
| | 20 | 123 | | uint64_t qt_webview_create(uint64_t window_id, rust::Str url) { |
| | | 124 | | // The view is created unparented; the window agent embeds it through |
| | | 125 | | // qt_window_attach_webview(window_id, qt_webview_native_handle(id)). |
| | 20 | 126 | | (void)window_id; |
| | | 127 | | |
| | 20 | 128 | | auto* view = new QWebEngineView(); |
| | 20 | 129 | | const uint64_t id = g_next_id++; |
| | | 130 | | |
| | 20 | 131 | | Entry entry; |
| | 20 | 132 | | entry.view = view; |
| | 20 | 133 | | g_webviews.emplace(id, entry); |
| | | 134 | | |
| | 20 | 135 | | QWebEnginePage* page = view->page(); |
| | 20 | 136 | | QObject::connect(page, &QWebEnginePage::loadStarted, view, |
| | 20 | 137 | | [id, view]() { emit_event(id, 1, view->url().toString()); }); |
| | 20 | 138 | | QObject::connect(page, &QWebEnginePage::loadFinished, view, [id, view](bool ok) { |
| | 20 | 139 | | emit_event(id, 2, view->url().toString()); |
| | 20 | 140 | | if (!ok) { |
| | 0 | 141 | | emit_event(id, 5, view->url().toString()); |
| | 0 | 142 | | } |
| | 20 | 143 | | }); |
| | 20 | 144 | | QObject::connect(page, &QWebEnginePage::urlChanged, view, |
| | 20 | 145 | | [id](const QUrl& u) { emit_event(id, 3, u.toString()); }); |
| | 20 | 146 | | QObject::connect(page, &QWebEnginePage::titleChanged, view, |
| | 60 | 147 | | [id](const QString& t) { emit_event(id, 4, t); }); |
| | | 148 | | |
| | | 149 | | // TAURIQT_EVAL_DEBUG smoke test: for the first webview, ask Rust once when |
| | | 150 | | // the first page load finishes. Rust checks the environment variable and, |
| | | 151 | | // when it is `1`, runs `document.title = 'eval-ok'` through the regular |
| | | 152 | | // eval_with_callback path and prints the callback result (see |
| | | 153 | | // `ops/webview.rs::debug_hook_first_load`). Without the variable this is a |
| | | 154 | | // single cheap call into Rust per process. |
| | 20 | 155 | | if (id == 1) { |
| | 20 | 156 | | QObject::connect(page, &QWebEnginePage::loadFinished, view, [id](bool) { |
| | 20 | 157 | | static bool fired = false; |
| | 20 | 158 | | if (!fired) { |
| | 20 | 159 | | fired = true; |
| | 20 | 160 | | qt_webview_on_first_load(id); |
| | 20 | 161 | | } |
| | 20 | 162 | | }); |
| | 20 | 163 | | } |
| | | 164 | | |
| | | 165 | | // Let the scheme layer find this webview's profile for handler install, and |
| | | 166 | | // install the handlers before the first navigation: a `tauri://localhost` |
| | | 167 | | // load issued earlier would be rejected for having no scheme handler. |
| | 20 | 168 | | qt_scheme_bind_webview(id, view); |
| | 20 | 169 | | qt_scheme_install_handler(id); |
| | | 170 | | |
| | | 171 | | // Backstop: if the widget is destroyed behind our back, drop the handle. |
| | 20 | 172 | | QObject::connect(view, &QObject::destroyed, view, [id]() { g_webviews.erase(id); }); |
| | | 173 | | |
| | 20 | 174 | | if (url.size() != 0) { |
| | 0 | 175 | | view->load(QUrl(to_qstring(url))); |
| | 0 | 176 | | } |
| | 20 | 177 | | return id; |
| | 20 | 178 | | } |
| | | 179 | | |
| | 20 | 180 | | uint64_t qt_webview_native_handle(uint64_t id) { |
| | 20 | 181 | | Entry* e = find_entry(id); |
| | 20 | 182 | | return e != nullptr ? reinterpret_cast<uint64_t>(e->view) : 0; |
| | 20 | 183 | | } |
| | | 184 | | |
| | 20 | 185 | | void qt_webview_navigate(uint64_t id, rust::Str url) { |
| | 20 | 186 | | Entry* e = find_entry(id); |
| | 20 | 187 | | if (e == nullptr) { |
| | 0 | 188 | | return; |
| | 0 | 189 | | } |
| | 20 | 190 | | e->view->load(QUrl(to_qstring(url))); |
| | 20 | 191 | | } |
| | | 192 | | |
| | 60 | 193 | | rust::String qt_webview_url(uint64_t id) { |
| | 60 | 194 | | Entry* e = find_entry(id); |
| | 60 | 195 | | if (e == nullptr) { |
| | 0 | 196 | | return rust::String(); |
| | 0 | 197 | | } |
| | 60 | 198 | | return rust::String(e->view->url().toString().toStdString()); |
| | 60 | 199 | | } |
| | | 200 | | |
| | 0 | 201 | | void qt_webview_reload(uint64_t id) { |
| | 0 | 202 | | Entry* e = find_entry(id); |
| | 0 | 203 | | if (e == nullptr) { |
| | 0 | 204 | | return; |
| | 0 | 205 | | } |
| | 0 | 206 | | e->view->reload(); |
| | 0 | 207 | | } |
| | | 208 | | |
| | 0 | 209 | | void qt_webview_eval(uint64_t id, rust::Str script) { |
| | 0 | 210 | | Entry* e = find_entry(id); |
| | 0 | 211 | | if (e == nullptr) { |
| | 0 | 212 | | return; |
| | 0 | 213 | | } |
| | | 214 | | // Default worldId = 0 (MainWorld), fire-and-forget (no result callback). |
| | 0 | 215 | | e->view->page()->runJavaScript(to_qstring(script)); |
| | 0 | 216 | | } |
| | | 217 | | |
| | 180 | 218 | | void qt_webview_add_init_script(uint64_t id, rust::Str script, bool main_frame_only) { |
| | 180 | 219 | | Entry* e = find_entry(id); |
| | 180 | 220 | | if (e == nullptr) { |
| | 0 | 221 | | return; |
| | 0 | 222 | | } |
| | | 223 | | // Per-page collection; consumed by the next navigation, so this must run |
| | | 224 | | // before the first load() for tauri's bootstrap scripts to see the page. |
| | 180 | 225 | | QWebEngineScript s; |
| | 180 | 226 | | s.setName(QStringLiteral("tauri_init_%1").arg(g_next_script_id++)); |
| | 180 | 227 | | s.setSourceCode(to_qstring(script)); |
| | 180 | 228 | | s.setInjectionPoint(QWebEngineScript::DocumentCreation); |
| | 180 | 229 | | s.setWorldId(QWebEngineScript::MainWorld); |
| | 180 | 230 | | s.setRunsOnSubFrames(!main_frame_only); |
| | 180 | 231 | | e->view->page()->scripts().insert(s); |
| | 180 | 232 | | } |
| | | 233 | | |
| | 0 | 234 | | void qt_webview_set_zoom(uint64_t id, double factor) { |
| | 0 | 235 | | Entry* e = find_entry(id); |
| | 0 | 236 | | if (e == nullptr) { |
| | 0 | 237 | | return; |
| | 0 | 238 | | } |
| | 0 | 239 | | e->view->page()->setZoomFactor(static_cast<qreal>(factor)); |
| | 0 | 240 | | } |
| | | 241 | | |
| | 0 | 242 | | void qt_webview_set_background_color(uint64_t id, int r, int g, int b, int a) { |
| | 0 | 243 | | Entry* e = find_entry(id); |
| | 0 | 244 | | if (e == nullptr) { |
| | 0 | 245 | | return; |
| | 0 | 246 | | } |
| | 0 | 247 | | e->view->page()->setBackgroundColor(QColor(r, g, b, a)); |
| | 0 | 248 | | } |
| | | 249 | | |
| | 0 | 250 | | void qt_webview_show(uint64_t id) { |
| | 0 | 251 | | Entry* e = find_entry(id); |
| | 0 | 252 | | if (e == nullptr) { |
| | 0 | 253 | | return; |
| | 0 | 254 | | } |
| | 0 | 255 | | e->view->show(); |
| | 0 | 256 | | e->view->raise(); |
| | 0 | 257 | | } |
| | | 258 | | |
| | 0 | 259 | | void qt_webview_hide(uint64_t id) { |
| | 0 | 260 | | Entry* e = find_entry(id); |
| | 0 | 261 | | if (e == nullptr) { |
| | 0 | 262 | | return; |
| | 0 | 263 | | } |
| | 0 | 264 | | e->view->hide(); |
| | 0 | 265 | | } |
| | | 266 | | |
| | 0 | 267 | | QtWebviewGeometry qt_webview_geometry(uint64_t id) { |
| | 0 | 268 | | QtWebviewGeometry geometry{}; |
| | 0 | 269 | | Entry* e = find_entry(id); |
| | 0 | 270 | | if (e == nullptr || e->view == nullptr) { |
| | 0 | 271 | | geometry.ok = false; // all other fields stay zero |
| | 0 | 272 | | return geometry; |
| | 0 | 273 | | } |
| | | 274 | | |
| | | 275 | | // `QWidget::geometry()` is in device-independent pixels; tauri's |
| | | 276 | | // `Physical*` values are device pixels, so scale by the widget's dpr. |
| | | 277 | | // `QWidget::devicePixelRatioF()` follows the widget's window, which is the |
| | | 278 | | // correct scale under Wayland fractional scaling (a `QScreen`'s dpr can |
| | | 279 | | // differ from the window's there). |
| | 0 | 280 | | const QRect rect = e->view->geometry(); |
| | 0 | 281 | | const qreal dpr = e->view->devicePixelRatioF(); |
| | 0 | 282 | | geometry.ok = true; |
| | 0 | 283 | | geometry.x = static_cast<std::int32_t>(std::lround(rect.x() * dpr)); |
| | 0 | 284 | | geometry.y = static_cast<std::int32_t>(std::lround(rect.y() * dpr)); |
| | 0 | 285 | | geometry.width = static_cast<std::int32_t>(std::lround(rect.width() * dpr)); |
| | 0 | 286 | | geometry.height = static_cast<std::int32_t>(std::lround(rect.height() * dpr)); |
| | 0 | 287 | | return geometry; |
| | 0 | 288 | | } |
| | | 289 | | |
| | 0 | 290 | | bool qt_webview_set_size(uint64_t id, int width, int height, bool physical) { |
| | 0 | 291 | | Entry* e = find_entry(id); |
| | 0 | 292 | | if (e == nullptr || e->view == nullptr) { |
| | 0 | 293 | | return false; |
| | 0 | 294 | | } |
| | 0 | 295 | | detach_from_layout(e->view); |
| | 0 | 296 | | const qreal dpr = e->view->devicePixelRatioF(); |
| | 0 | 297 | | const int logical_width = physical ? static_cast<int>(std::lround(width / dpr)) : width; |
| | 0 | 298 | | const int logical_height = physical ? static_cast<int>(std::lround(height / dpr)) : height; |
| | 0 | 299 | | e->view->resize(logical_width, logical_height); |
| | 0 | 300 | | return true; |
| | 0 | 301 | | } |
| | | 302 | | |
| | 0 | 303 | | bool qt_webview_set_position(uint64_t id, int x, int y, bool physical) { |
| | 0 | 304 | | Entry* e = find_entry(id); |
| | 0 | 305 | | if (e == nullptr || e->view == nullptr) { |
| | 0 | 306 | | return false; |
| | 0 | 307 | | } |
| | 0 | 308 | | detach_from_layout(e->view); |
| | 0 | 309 | | const qreal dpr = e->view->devicePixelRatioF(); |
| | 0 | 310 | | const int logical_x = physical ? static_cast<int>(std::lround(x / dpr)) : x; |
| | 0 | 311 | | const int logical_y = physical ? static_cast<int>(std::lround(y / dpr)) : y; |
| | | 312 | | // QWidget::move is relative to the parent widget, i.e. to the window's |
| | | 313 | | // client-area origin, which is what tauri's webview position means. |
| | 0 | 314 | | e->view->move(logical_x, logical_y); |
| | 0 | 315 | | return true; |
| | 0 | 316 | | } |
| | | 317 | | |
| | 0 | 318 | | bool qt_webview_set_focus(uint64_t id) { |
| | 0 | 319 | | Entry* e = find_entry(id); |
| | 0 | 320 | | if (e == nullptr || e->view == nullptr) { |
| | 0 | 321 | | return false; |
| | 0 | 322 | | } |
| | 0 | 323 | | e->view->setFocus(); |
| | 0 | 324 | | return true; |
| | 0 | 325 | | } |
| | | 326 | | |
| | 0 | 327 | | bool qt_webview_eval_with_callback(uint64_t id, uint64_t request_id, rust::Str script) { |
| | 0 | 328 | | Entry* e = find_entry(id); |
| | 0 | 329 | | if (e == nullptr || e->view == nullptr || e->view->page() == nullptr) { |
| | 0 | 330 | | return false; |
| | 0 | 331 | | } |
| | | 332 | | |
| | 0 | 333 | | const QString source = to_qstring(script); |
| | 0 | 334 | | e->view->page()->runJavaScript(source, [request_id](const QVariant& value) { |
| | | 335 | | // Qt hands the JS completion value back as a QVariant; `json_text` |
| | | 336 | | // serializes it as a JSON string so Rust/tauri see the same shape wry |
| | | 337 | | // produces (an undefined/unrepresentable value becomes `null`). |
| | 0 | 338 | | const QByteArray json = json_text(value); |
| | 0 | 339 | | qt_on_eval_result(request_id, |
| | 0 | 340 | | rust::Str(json.constData(), static_cast<std::size_t>(json.size()))); |
| | 0 | 341 | | }); |
| | 0 | 342 | | return true; |
| | 0 | 343 | | } |
| | | 344 | | |
| | 0 | 345 | | bool qt_webview_reparent(uint64_t id, uint64_t window_native) { |
| | 0 | 346 | | Entry* e = find_entry(id); |
| | 0 | 347 | | if (e == nullptr || e->view == nullptr) { |
| | 0 | 348 | | return false; |
| | 0 | 349 | | } |
| | | 350 | | |
| | | 351 | | // The window registry's accessors return 0 for an unknown handle, and a |
| | | 352 | | // window that exists always has a native window id, so `winId() == 0` is a |
| | | 353 | | // truthful "no such window" probe (there is no `qt_window_widget()` accessor |
| | | 354 | | // to ask directly). |
| | 0 | 355 | | if (qt_window_winid(window_native) == 0) { |
| | 0 | 356 | | return false; |
| | 0 | 357 | | } |
| | | 358 | | |
| | | 359 | | // The window agent adopts the widget: `qt_window_attach_webview()` reparents |
| | | 360 | | // it into the target window (dropping it from its previous layout), |
| | | 361 | | // (re)creates the window's zero-margin layout, adds the view and shows it. |
| | | 362 | | // That is exactly "reparent into another window", and it puts the view back |
| | | 363 | | // under layout management (see `detach_from_layout`). |
| | 0 | 364 | | detach_from_layout(e->view); |
| | 0 | 365 | | qt_window_attach_webview( |
| | 0 | 366 | | window_native, static_cast<uint64_t>(reinterpret_cast<quintptr>(e->view))); |
| | 0 | 367 | | return true; |
| | 0 | 368 | | } |
| | | 369 | | |
| | 0 | 370 | | bool qt_webview_clear_browsing_data(uint64_t id) { |
| | 0 | 371 | | Entry* e = find_entry(id); |
| | 0 | 372 | | if (e == nullptr || e->view == nullptr || e->view->page() == nullptr) { |
| | 0 | 373 | | return false; |
| | 0 | 374 | | } |
| | 0 | 375 | | QWebEngineProfile* profile = e->view->page()->profile(); |
| | 0 | 376 | | if (profile == nullptr) { |
| | 0 | 377 | | return false; |
| | 0 | 378 | | } |
| | | 379 | | |
| | | 380 | | // What Qt 6.11 can clear on a live profile: |
| | | 381 | | // * the HTTP cache (in-memory for off-the-record profiles), |
| | | 382 | | // * every cookie of the profile's cookie store, |
| | | 383 | | // * the visited-links list. |
| | | 384 | | // Both cache clearing and cookie deletion are asynchronous inside |
| | | 385 | | // QtWebEngine: `clearHttpCache()` signals `clearHttpCacheCompleted()` and |
| | | 386 | | // `deleteAllCookies()` has no completion signal at all, so this is |
| | | 387 | | // fire-and-forget by design (the tauri method returns `Result<()>` and |
| | | 388 | | // blocking the GUI thread on the GUI thread's own work would deadlock). |
| | | 389 | | // |
| | | 390 | | // What it CANNOT clear (no API on a live profile): |
| | | 391 | | // * localStorage / sessionStorage, |
| | | 392 | | // * IndexedDB databases, |
| | | 393 | | // * service-worker registrations and Cache Storage, |
| | | 394 | | // * WebSQL is gone in Chromium, and HTTP auth caches are not exposed. |
| | | 395 | | // Those live under the profile's `persistentStoragePath()`; the only way to |
| | | 396 | | // drop them is to remove that directory while no profile is alive, which |
| | | 397 | | // this runtime does not do. Note also that tauri's `clear_all_browsing_data` |
| | | 398 | | // is documented as clearing "all" browsing data; on this runtime it clears |
| | | 399 | | // the list above and leaves the listed stores in place. |
| | 0 | 400 | | profile->clearHttpCache(); |
| | 0 | 401 | | profile->clearAllVisitedLinks(); |
| | 0 | 402 | | if (QWebEngineCookieStore* store = profile->cookieStore()) { |
| | 0 | 403 | | store->deleteAllCookies(); |
| | 0 | 404 | | } |
| | 0 | 405 | | return true; |
| | 0 | 406 | | } |
| | | 407 | | |
| | 0 | 408 | | void qt_webview_open_devtools(uint64_t id) { |
| | 0 | 409 | | Entry* e = find_entry(id); |
| | 0 | 410 | | if (e == nullptr || e->devtools_view != nullptr) { |
| | 0 | 411 | | return; |
| | 0 | 412 | | } |
| | | 413 | | |
| | | 414 | | // Qt's only inspector mechanism: bind a second page as the devtools page of |
| | | 415 | | // the inspected page; binding navigates it to the DevTools frontend. |
| | 0 | 416 | | QWebEnginePage* page = e->view->page(); |
| | 0 | 417 | | auto* dev_page = new QWebEnginePage(page->profile(), e->view); |
| | 0 | 418 | | auto* dev_view = new QWebEngineView(dev_page); |
| | 0 | 419 | | page->setDevToolsPage(dev_page); |
| | 0 | 420 | | dev_view->resize(1000, 720); |
| | 0 | 421 | | dev_view->show(); |
| | | 422 | | |
| | 0 | 423 | | e->devtools_page = dev_page; |
| | 0 | 424 | | e->devtools_view = dev_view; |
| | | 425 | | |
| | 0 | 426 | | QObject::connect(dev_view, &QObject::destroyed, dev_view, [id]() { |
| | 0 | 427 | | Entry* cur = find_entry(id); |
| | 0 | 428 | | if (cur != nullptr) { |
| | 0 | 429 | | cur->devtools_view = nullptr; |
| | 0 | 430 | | cur->devtools_page = nullptr; |
| | 0 | 431 | | } |
| | 0 | 432 | | }); |
| | 0 | 433 | | } |
| | | 434 | | |
| | 0 | 435 | | void qt_webview_close_devtools(uint64_t id) { |
| | 0 | 436 | | Entry* e = find_entry(id); |
| | 0 | 437 | | if (e == nullptr) { |
| | 0 | 438 | | return; |
| | 0 | 439 | | } |
| | 0 | 440 | | if (e->view != nullptr && e->view->page() != nullptr) { |
| | | 441 | | // Unbinds the devtools page (clears its inspected page too). |
| | 0 | 442 | | e->view->page()->setDevToolsPage(nullptr); |
| | 0 | 443 | | } |
| | 0 | 444 | | if (e->devtools_view != nullptr) { |
| | 0 | 445 | | e->devtools_view->hide(); |
| | 0 | 446 | | e->devtools_view->deleteLater(); |
| | 0 | 447 | | e->devtools_view = nullptr; |
| | 0 | 448 | | } |
| | 0 | 449 | | if (e->devtools_page != nullptr) { |
| | | 450 | | // Dev page is parented to the main view; delete it after its view. |
| | 0 | 451 | | e->devtools_page->deleteLater(); |
| | 0 | 452 | | e->devtools_page = nullptr; |
| | 0 | 453 | | } |
| | 0 | 454 | | } |
| | | 455 | | |
| | 0 | 456 | | bool qt_webview_is_devtools_open(uint64_t id) { |
| | 0 | 457 | | Entry* e = find_entry(id); |
| | 0 | 458 | | return e != nullptr && e->devtools_view != nullptr && e->devtools_view->isVisible(); |
| | 0 | 459 | | } |
| | | 460 | | |
| | 0 | 461 | | void qt_webview_print(uint64_t id) { |
| | 0 | 462 | | Entry* e = find_entry(id); |
| | 0 | 463 | | if (e == nullptr) { |
| | 0 | 464 | | return; |
| | 0 | 465 | | } |
| | | 466 | | // Qt 6.11 has no print->dialog API; emulate wry with QPrinter + |
| | | 467 | | // QPrintDialog and print the page on accept. Requires user interaction. |
| | 0 | 468 | | QPrinter printer(QPrinter::HighResolution); |
| | 0 | 469 | | QPrintDialog dialog(&printer, e->view); |
| | 0 | 470 | | if (dialog.exec() == QDialog::Accepted) { |
| | 0 | 471 | | e->view->print(&printer); |
| | 0 | 472 | | } |
| | 0 | 473 | | } |
| | | 474 | | |
| | 0 | 475 | | void qt_webview_close(uint64_t id) { |
| | 0 | 476 | | auto it = g_webviews.find(id); |
| | 0 | 477 | | if (it == g_webviews.end()) { |
| | 0 | 478 | | return; |
| | 0 | 479 | | } |
| | 0 | 480 | | Entry e = it->second; |
| | 0 | 481 | | g_webviews.erase(it); |
| | | 482 | | |
| | 0 | 483 | | if (e.view != nullptr && e.view->page() != nullptr) { |
| | 0 | 484 | | e.view->page()->setDevToolsPage(nullptr); |
| | 0 | 485 | | } |
| | 0 | 486 | | if (e.devtools_view != nullptr) { |
| | 0 | 487 | | e.devtools_view->hide(); |
| | 0 | 488 | | e.devtools_view->deleteLater(); |
| | 0 | 489 | | } |
| | 0 | 490 | | if (e.devtools_page != nullptr) { |
| | 0 | 491 | | e.devtools_page->deleteLater(); |
| | 0 | 492 | | } |
| | 0 | 493 | | if (e.view != nullptr) { |
| | | 494 | | // QWebEngineView owns the page it created in its default constructor. |
| | 0 | 495 | | e.view->hide(); |
| | 0 | 496 | | e.view->deleteLater(); |
| | 0 | 497 | | } |
| | 0 | 498 | | } |