fit-webview-bridge 0.2.0a4__tar.gz → 0.2.1a3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fit-webview-bridge might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fit-webview-bridge
3
- Version: 0.2.0a4
3
+ Version: 0.2.1a3
4
4
  Summary: Qt native WebView bridge with PySide6 bindings
5
5
  Author: FIT Project
6
6
  License: LGPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fit-webview-bridge"
3
- version = "0.2.0a4"
3
+ version = "0.2.1a3"
4
4
  description = "Qt native WebView bridge with PySide6 bindings"
5
5
  requires-python = ">=3.11,<3.14"
6
6
  dependencies = ["PySide6==6.9.0", "shiboken6==6.9.0", "shiboken6-generator==6.9.0"]
@@ -22,6 +22,9 @@ public:
22
22
  Q_INVOKABLE void reload();
23
23
  Q_INVOKABLE void evaluateJavaScript(const QString& script);
24
24
 
25
+ Q_INVOKABLE void setDownloadDirectory(const QString& dirPath);
26
+ Q_INVOKABLE QString downloadDirectory() const;
27
+
25
28
  signals:
26
29
  void loadFinished(bool ok);
27
30
  void urlChanged(const QUrl& url);
@@ -30,6 +33,12 @@ signals:
30
33
  void canGoBackChanged(bool);
31
34
  void canGoForwardChanged(bool);
32
35
 
36
+
37
+ void downloadStarted(const QString& suggestedFilename, const QString& destinationPath);
38
+ void downloadProgress(qint64 bytesReceived, qint64 totalBytes);
39
+ void downloadFinished(const QString& filePath);
40
+ void downloadFailed(const QString& filePath, const QString& error);
41
+
33
42
  protected:
34
43
  void showEvent(QShowEvent*) override;
35
44
  void resizeEvent(QResizeEvent*) override;
@@ -0,0 +1,409 @@
1
+ #import <Cocoa/Cocoa.h>
2
+ #import <WebKit/WebKit.h>
3
+
4
+ #include "WKWebViewWidget.h"
5
+
6
+ #include <QtWidgets>
7
+ #include <QString>
8
+ #include <QUrl>
9
+ #include <QDir>
10
+
11
+ @class WKNavDelegate;
12
+ @class FitUrlMsgHandler;
13
+
14
+ // =======================
15
+ // Impl
16
+ // =======================
17
+ struct WKWebViewWidget::Impl {
18
+ WKWebView* wk = nil;
19
+ WKNavDelegate* delegate = nil;
20
+ WKUserContentController* ucc = nil;
21
+ FitUrlMsgHandler* msg = nil;
22
+ QString downloadDir; // es. ~/Downloads
23
+ };
24
+
25
+ // =======================
26
+ // Helpers forward
27
+ // =======================
28
+ static NSURL* toNSURL(QUrl u);
29
+
30
+ // =======================
31
+ // SPA message handler
32
+ // =======================
33
+ @interface FitUrlMsgHandler : NSObject <WKScriptMessageHandler>
34
+ @property(nonatomic, assign) WKWebViewWidget* owner;
35
+ @end
36
+
37
+ @implementation FitUrlMsgHandler
38
+ - (void)userContentController:(WKUserContentController *)userContentController
39
+ didReceiveScriptMessage:(WKScriptMessage *)message {
40
+ if (!self.owner) return;
41
+ if (![message.name isEqualToString:@"fitUrlChanged"]) return;
42
+ if (![message.body isKindOfClass:[NSString class]]) return;
43
+ QString s = QString::fromUtf8([(NSString*)message.body UTF8String]);
44
+ emit self.owner->urlChanged(QUrl::fromEncoded(s.toUtf8()));
45
+ }
46
+ @end
47
+
48
+ // =======================
49
+ // Navigation + Download delegate
50
+ // =======================
51
+ @interface WKNavDelegate : NSObject <WKNavigationDelegate, WKDownloadDelegate>
52
+ @property(nonatomic, assign) WKWebViewWidget* owner;
53
+ // stato download (nel delegate, non toccare i privati del widget)
54
+ @property(nonatomic, strong) NSMapTable<WKDownload*, NSString*>* downloadPaths;
55
+ @end
56
+
57
+ @implementation WKNavDelegate
58
+
59
+ - (instancetype)init {
60
+ if ((self = [super init])) {
61
+ _downloadPaths = [NSMapTable weakToStrongObjectsMapTable];
62
+ }
63
+ return self;
64
+ }
65
+
66
+ #pragma mark - Navigazione
67
+
68
+ - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
69
+ if (!self.owner) return;
70
+ if (webView.URL)
71
+ emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
72
+ emit self.owner->loadProgress(5);
73
+ emit self.owner->canGoBackChanged(webView.canGoBack);
74
+ emit self.owner->canGoForwardChanged(webView.canGoForward);
75
+ }
76
+
77
+ - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
78
+ if (!self.owner) return;
79
+ if (webView.URL)
80
+ emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
81
+ emit self.owner->loadProgress(50);
82
+ emit self.owner->canGoBackChanged(webView.canGoBack);
83
+ emit self.owner->canGoForwardChanged(webView.canGoForward);
84
+ }
85
+
86
+ - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
87
+ if (!self.owner) return;
88
+ if (webView.URL)
89
+ emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
90
+ }
91
+
92
+ - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
93
+ if (!self.owner) return;
94
+ emit self.owner->loadFinished(true);
95
+ if (webView.URL)
96
+ emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
97
+ if (webView.title)
98
+ emit self.owner->titleChanged(QString::fromUtf8(webView.title.UTF8String));
99
+ emit self.owner->loadProgress(100);
100
+ emit self.owner->canGoBackChanged(webView.canGoBack);
101
+ emit self.owner->canGoForwardChanged(webView.canGoForward);
102
+ }
103
+
104
+ - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
105
+ if (!self.owner) return;
106
+ emit self.owner->loadFinished(false);
107
+ emit self.owner->loadProgress(0);
108
+ emit self.owner->canGoBackChanged(webView.canGoBack);
109
+ emit self.owner->canGoForwardChanged(webView.canGoForward);
110
+ }
111
+
112
+ #pragma mark - Decide download vs render
113
+
114
+ - (void)webView:(WKWebView *)webView
115
+ decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
116
+ decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
117
+ {
118
+ if (navigationResponse.canShowMIMEType) {
119
+ decisionHandler(WKNavigationResponsePolicyAllow);
120
+ } else {
121
+ // API moderna: "Download" (non BecomeDownload)
122
+ decisionHandler(WKNavigationResponsePolicyDownload);
123
+ }
124
+ }
125
+
126
+ #pragma mark - Diventare download
127
+
128
+ - (void)webView:(WKWebView *)webView
129
+ navigationAction:(WKNavigationAction *)navigationAction
130
+ didBecomeDownload:(WKDownload *)download
131
+ {
132
+ download.delegate = self;
133
+ if (self.owner) {
134
+ emit self.owner->downloadStarted(QString(), QString());
135
+ }
136
+
137
+ [download.progress addObserver:self forKeyPath:@"fractionCompleted"
138
+ options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial)
139
+ context:NULL];
140
+ [download.progress addObserver:self forKeyPath:@"completedUnitCount"
141
+ options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial)
142
+ context:NULL];
143
+ [download.progress addObserver:self forKeyPath:@"totalUnitCount"
144
+ options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial)
145
+ context:NULL];
146
+ }
147
+
148
+ - (void)webView:(WKWebView *)webView
149
+ navigationResponse:(WKNavigationResponse *)navigationResponse
150
+ didBecomeDownload:(WKDownload *)download
151
+ {
152
+ download.delegate = self;
153
+
154
+ NSString* suggested = navigationResponse.response.suggestedFilename ?: @"download";
155
+ if (self.owner) {
156
+ QString destDir = self.owner->downloadDirectory();
157
+ QString destPath = destDir + "/" + QString::fromUtf8(suggested.UTF8String);
158
+ emit self.owner->downloadStarted(QString::fromUtf8(suggested.UTF8String),
159
+ destPath);
160
+ }
161
+
162
+ [download.progress addObserver:self forKeyPath:@"fractionCompleted"
163
+ options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial)
164
+ context:NULL];
165
+ [download.progress addObserver:self forKeyPath:@"completedUnitCount"
166
+ options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial)
167
+ context:NULL];
168
+ [download.progress addObserver:self forKeyPath:@"totalUnitCount"
169
+ options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial)
170
+ context:NULL];
171
+ }
172
+
173
+ #pragma mark - Scegli destinazione
174
+
175
+ static NSString* uniquePath(NSString* baseDir, NSString* filename) {
176
+ NSString* fname = filename ?: @"download";
177
+ NSString* path = [baseDir stringByAppendingPathComponent:fname];
178
+ NSFileManager* fm = [NSFileManager defaultManager];
179
+ if (![fm fileExistsAtPath:path]) return path;
180
+
181
+ NSString* name = [fname stringByDeletingPathExtension];
182
+ NSString* ext = [fname pathExtension];
183
+ for (NSUInteger i = 1; i < 10000; ++i) {
184
+ NSString* cand = ext.length
185
+ ? [NSString stringWithFormat:@"%@ (%lu).%@", name, (unsigned long)i, ext]
186
+ : [NSString stringWithFormat:@"%@ (%lu)", name, (unsigned long)i];
187
+ NSString* candPath = [baseDir stringByAppendingPathComponent:cand];
188
+ if (![fm fileExistsAtPath:candPath]) return candPath;
189
+ }
190
+ return path;
191
+ }
192
+
193
+ - (void)download:(WKDownload *)download
194
+ decideDestinationUsingResponse:(NSURLResponse *)response
195
+ suggestedFilename:(NSString *)suggestedFilename
196
+ completionHandler:(void (^)(NSURL * _Nullable destination))completionHandler
197
+ {
198
+ if (!self.owner) { completionHandler(nil); return; }
199
+
200
+ QString qdir = self.owner->downloadDirectory();
201
+ NSString* dir = [NSString stringWithUTF8String:qdir.toUtf8().constData()];
202
+ if (!dir.length) {
203
+ dir = [NSHomeDirectory() stringByAppendingPathComponent:@"Downloads"];
204
+ }
205
+
206
+ [[NSFileManager defaultManager] createDirectoryAtPath:dir
207
+ withIntermediateDirectories:YES
208
+ attributes:nil error:nil];
209
+
210
+ NSString* finalPath = uniquePath(dir, suggestedFilename ?: @"download");
211
+ [self.downloadPaths setObject:finalPath forKey:download];
212
+
213
+ emit self.owner->downloadStarted(
214
+ QString::fromUtf8((suggestedFilename ?: @"download").UTF8String),
215
+ QString::fromUtf8(finalPath.UTF8String)
216
+ );
217
+
218
+ completionHandler([NSURL fileURLWithPath:finalPath]);
219
+ }
220
+
221
+ #pragma mark - Progress / Fine / Errore
222
+
223
+ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)obj
224
+ change:(NSDictionary *)change context:(void *)ctx
225
+ {
226
+ if (![obj isKindOfClass:[NSProgress class]] || !self.owner) {
227
+ [super observeValueForKeyPath:keyPath ofObject:obj change:change context:ctx];
228
+ return;
229
+ }
230
+
231
+ NSProgress* prog = (NSProgress*)obj;
232
+ int64_t total = prog.totalUnitCount; // può essere -1 (sconosciuto)
233
+ int64_t done = prog.completedUnitCount;
234
+
235
+ // Emetti SEMPRE su main (thread-safety Qt/UI)
236
+ dispatch_async(dispatch_get_main_queue(), ^{
237
+ emit self.owner->downloadProgress(done, (total >= 0 ? total : -1));
238
+ });
239
+ }
240
+
241
+
242
+ - (void)downloadDidFinish:(WKDownload *)download {
243
+ if (!self.owner) return;
244
+
245
+ @try {
246
+ [download.progress removeObserver:self forKeyPath:@"fractionCompleted"];
247
+ [download.progress removeObserver:self forKeyPath:@"completedUnitCount"];
248
+ [download.progress removeObserver:self forKeyPath:@"totalUnitCount"];
249
+ } @catch (...) {}
250
+
251
+ NSString* finalPath = [self.downloadPaths objectForKey:download];
252
+ if (finalPath) {
253
+ emit self.owner->downloadFinished(QString::fromUtf8(finalPath.UTF8String));
254
+ [self.downloadPaths removeObjectForKey:download];
255
+ } else {
256
+ emit self.owner->downloadFinished(QString());
257
+ }
258
+ }
259
+
260
+ - (void)download:(WKDownload *)download didFailWithError:(NSError *)error resumeData:(NSData *)resumeData {
261
+ if (!self.owner) return;
262
+
263
+ @try {
264
+ [download.progress removeObserver:self forKeyPath:@"fractionCompleted"];
265
+ [download.progress removeObserver:self forKeyPath:@"completedUnitCount"];
266
+ [download.progress removeObserver:self forKeyPath:@"totalUnitCount"];
267
+ } @catch (...) {}
268
+
269
+ NSString* finalPath = [self.downloadPaths objectForKey:download];
270
+ QString qpath = finalPath ? QString::fromUtf8(finalPath.UTF8String) : QString();
271
+ emit self.owner->downloadFailed(qpath, QString::fromUtf8(error.localizedDescription.UTF8String));
272
+ if (finalPath) [self.downloadPaths removeObjectForKey:download];
273
+ }
274
+
275
+ @end
276
+
277
+ // =======================
278
+ // QUrl -> NSURL (normalizza e forza https)
279
+ // =======================
280
+ static NSURL* toNSURL(QUrl u) {
281
+ if (!u.isValid()) return nil;
282
+
283
+ if (u.scheme().isEmpty())
284
+ u = QUrl::fromUserInput(u.toString());
285
+
286
+ // Forza sempre http -> https (nessuna eccezione)
287
+ if (u.scheme() == "http")
288
+ u.setScheme("https");
289
+
290
+ if (u.isLocalFile())
291
+ return [NSURL fileURLWithPath:[NSString stringWithUTF8String:u.toLocalFile().toUtf8().constData()]];
292
+
293
+ const QByteArray enc = u.toString(QUrl::FullyEncoded).toUtf8();
294
+ return [NSURL URLWithString:[NSString stringWithUTF8String:enc.constData()]];
295
+ }
296
+
297
+ // =======================
298
+ // WKWebViewWidget
299
+ // =======================
300
+ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
301
+ : QWidget(parent), d(new Impl) {
302
+ setAttribute(Qt::WA_NativeWindow, true);
303
+ (void)winId();
304
+
305
+ d->downloadDir = QDir::homePath() + "/Downloads";
306
+
307
+ NSView* nsParent = (__bridge NSView*)reinterpret_cast<void*>(winId());
308
+ WKWebViewConfiguration* cfg = [[WKWebViewConfiguration alloc] init];
309
+ if ([cfg respondsToSelector:@selector(defaultWebpagePreferences)]) {
310
+ cfg.defaultWebpagePreferences.allowsContentJavaScript = YES;
311
+ }
312
+
313
+ // SPA: intercetta pushState/replaceState/popstate/click
314
+ d->ucc = [WKUserContentController new];
315
+ d->msg = [FitUrlMsgHandler new];
316
+ d->msg.owner = this;
317
+ [d->ucc addScriptMessageHandler:d->msg name:@"fitUrlChanged"];
318
+
319
+ NSString* js =
320
+ @"(function(){"
321
+ @" function emit(){ try{ window.webkit.messageHandlers.fitUrlChanged.postMessage(location.href); }catch(e){} }"
322
+ @" var _ps = history.pushState; history.pushState = function(){ _ps.apply(this, arguments); emit(); };"
323
+ @" var _rs = history.replaceState; history.replaceState = function(){ _rs.apply(this, arguments); emit(); };"
324
+ @" window.addEventListener('popstate', emit, true);"
325
+ @" document.addEventListener('click', function(ev){"
326
+ @" var a = ev.target && ev.target.closest ? ev.target.closest('a[href]') : null;"
327
+ @" if (!a) return; if (a.target === '_blank' || a.hasAttribute('download')) return;"
328
+ @" setTimeout(emit, 0);"
329
+ @" }, true);"
330
+ @"})();";
331
+
332
+ WKUserScript* us = [[WKUserScript alloc]
333
+ initWithSource:js
334
+ injectionTime:WKUserScriptInjectionTimeAtDocumentStart
335
+ forMainFrameOnly:YES];
336
+ [d->ucc addUserScript:us];
337
+ cfg.userContentController = d->ucc;
338
+
339
+ d->wk = [[WKWebView alloc] initWithFrame:nsParent.bounds configuration:cfg];
340
+ d->wk.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
341
+ [nsParent addSubview:d->wk];
342
+
343
+ d->delegate = [WKNavDelegate new];
344
+ d->delegate.owner = this;
345
+ [d->wk setNavigationDelegate:d->delegate];
346
+ }
347
+
348
+ WKWebViewWidget::~WKWebViewWidget() {
349
+ if (!d) return;
350
+
351
+ if (d->ucc && d->msg) {
352
+ @try { [d->ucc removeScriptMessageHandlerForName:@"fitUrlChanged"]; } @catch (...) {}
353
+ }
354
+ d->msg = nil;
355
+
356
+ if (d->wk) { [d->wk removeFromSuperview]; d->wk = nil; }
357
+ d->delegate = nil;
358
+ d->ucc = nil;
359
+
360
+ delete d; d = nullptr;
361
+ }
362
+
363
+ void WKWebViewWidget::showEvent(QShowEvent* e) { QWidget::showEvent(e); }
364
+ void WKWebViewWidget::resizeEvent(QResizeEvent* e) { QWidget::resizeEvent(e); }
365
+
366
+ QUrl WKWebViewWidget::url() const {
367
+ if (!(d && d->wk)) return QUrl();
368
+ NSURL* nsurl = d->wk.URL;
369
+ if (!nsurl) return QUrl();
370
+ const char* utf8 = nsurl.absoluteString.UTF8String;
371
+ if (!utf8) return QUrl();
372
+ return QUrl::fromEncoded(QByteArray(utf8));
373
+ }
374
+
375
+ void WKWebViewWidget::setUrl(const QUrl& u) {
376
+ if (!(d && d->wk)) return;
377
+ NSURL* nsurl = toNSURL(u);
378
+ if (!nsurl) return;
379
+ [d->wk loadRequest:[NSURLRequest requestWithURL:nsurl]];
380
+ }
381
+
382
+ void WKWebViewWidget::back() { if (d && d->wk && d->wk.canGoBack) [d->wk goBack]; }
383
+ void WKWebViewWidget::forward() { if (d && d->wk && d->wk.canGoForward) [d->wk goForward]; }
384
+ void WKWebViewWidget::stop() { if (d && d->wk) [d->wk stopLoading:nil]; }
385
+ void WKWebViewWidget::reload() { if (d && d->wk) [d->wk reload]; }
386
+
387
+ void WKWebViewWidget::evaluateJavaScript(const QString& script) {
388
+ if (!d || !d->wk) return;
389
+ NSString* s = [NSString stringWithUTF8String:script.toUtf8().constData()];
390
+ [d->wk evaluateJavaScript:s completionHandler:^(id result, NSError* error){
391
+ Q_UNUSED(result); Q_UNUSED(error);
392
+ }];
393
+ }
394
+
395
+ // =======================
396
+ // Download directory API
397
+ // =======================
398
+ QString WKWebViewWidget::downloadDirectory() const {
399
+ return d ? d->downloadDir : QString();
400
+ }
401
+
402
+ void WKWebViewWidget::setDownloadDirectory(const QString& dirPath) {
403
+ if (!d) return;
404
+ QString p = QDir::fromNativeSeparators(dirPath);
405
+ if (p.endsWith('/')) p.chop(1);
406
+ if (p.isEmpty()) return;
407
+ QDir().mkpath(p);
408
+ d->downloadDir = p;
409
+ }
@@ -1,195 +0,0 @@
1
- #import <Cocoa/Cocoa.h>
2
- #import <WebKit/WebKit.h>
3
-
4
- #include "WKWebViewWidget.h"
5
-
6
- #include <QtWidgets>
7
- #include <QString>
8
- #include <QUrl>
9
-
10
- @class WKNavDelegate;
11
- @class FitUrlMsgHandler;
12
-
13
- struct WKWebViewWidget::Impl {
14
- WKWebView* wk = nil;
15
- WKNavDelegate* delegate = nil;
16
- WKUserContentController* ucc = nil;
17
- FitUrlMsgHandler* msg = nil;
18
- };
19
-
20
- @interface WKNavDelegate : NSObject <WKNavigationDelegate>
21
- @property(nonatomic, assign) WKWebViewWidget* owner;
22
- @end
23
-
24
- @implementation WKNavDelegate
25
- - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
26
- if (!self.owner) return;
27
- if (webView.URL)
28
- emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
29
- emit self.owner->loadProgress(5);
30
- emit self.owner->canGoBackChanged(webView.canGoBack);
31
- emit self.owner->canGoForwardChanged(webView.canGoForward);
32
- }
33
- - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
34
- if (!self.owner) return;
35
- if (webView.URL)
36
- emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
37
- emit self.owner->loadProgress(50);
38
- emit self.owner->canGoBackChanged(webView.canGoBack);
39
- emit self.owner->canGoForwardChanged(webView.canGoForward);
40
- }
41
- - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
42
- if (!self.owner) return;
43
- if (webView.URL)
44
- emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
45
- }
46
- - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
47
- if (!self.owner) return;
48
- emit self.owner->loadFinished(true);
49
- if (webView.URL)
50
- emit self.owner->urlChanged(QUrl::fromEncoded(QByteArray(webView.URL.absoluteString.UTF8String)));
51
- if (webView.title)
52
- emit self.owner->titleChanged(QString::fromUtf8(webView.title.UTF8String));
53
- emit self.owner->loadProgress(100);
54
- emit self.owner->canGoBackChanged(webView.canGoBack);
55
- emit self.owner->canGoForwardChanged(webView.canGoForward);
56
- }
57
- - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
58
- if (!self.owner) return;
59
- emit self.owner->loadFinished(false);
60
- emit self.owner->loadProgress(0);
61
- emit self.owner->canGoBackChanged(webView.canGoBack);
62
- emit self.owner->canGoForwardChanged(webView.canGoForward);
63
- }
64
- @end
65
-
66
- // Handler per messaggi JS (SPA: pushState/replaceState/popstate/click)
67
- @interface FitUrlMsgHandler : NSObject <WKScriptMessageHandler>
68
- @property(nonatomic, assign) WKWebViewWidget* owner;
69
- @end
70
-
71
- @implementation FitUrlMsgHandler
72
- - (void)userContentController:(WKUserContentController *)userContentController
73
- didReceiveScriptMessage:(WKScriptMessage *)message {
74
- if (!self.owner) return;
75
- if (![message.name isEqualToString:@"fitUrlChanged"]) return;
76
- if (![message.body isKindOfClass:[NSString class]]) return;
77
- QString s = QString::fromUtf8([(NSString*)message.body UTF8String]);
78
- emit self.owner->urlChanged(QUrl::fromEncoded(s.toUtf8()));
79
- }
80
- @end
81
-
82
- // Conversione QUrl -> NSURL con forzatura http->https e normalizzazione user-input
83
- static NSURL* toNSURL(QUrl u) {
84
- if (!u.isValid()) return nil;
85
-
86
- if (u.scheme().isEmpty())
87
- u = QUrl::fromUserInput(u.toString());
88
-
89
- // Forza sempre http -> https (nessuna eccezione)
90
- if (u.scheme() == "http")
91
- u.setScheme("https");
92
-
93
- if (u.isLocalFile())
94
- return [NSURL fileURLWithPath:[NSString stringWithUTF8String:u.toLocalFile().toUtf8().constData()]];
95
-
96
- const QByteArray enc = u.toString(QUrl::FullyEncoded).toUtf8();
97
- return [NSURL URLWithString:[NSString stringWithUTF8String:enc.constData()]];
98
- }
99
-
100
- WKWebViewWidget::WKWebViewWidget(QWidget* parent)
101
- : QWidget(parent), d(new Impl) {
102
- setAttribute(Qt::WA_NativeWindow, true);
103
- (void)winId();
104
-
105
- NSView* nsParent = (__bridge NSView*)reinterpret_cast<void*>(winId());
106
- WKWebViewConfiguration* cfg = [[WKWebViewConfiguration alloc] init];
107
- if ([cfg respondsToSelector:@selector(defaultWebpagePreferences)]) {
108
- cfg.defaultWebpagePreferences.allowsContentJavaScript = YES;
109
- }
110
-
111
- // UserContentController + script per intercettare navigazioni SPA
112
- d->ucc = [WKUserContentController new];
113
- d->msg = [FitUrlMsgHandler new];
114
- d->msg.owner = this;
115
- [d->ucc addScriptMessageHandler:d->msg name:@"fitUrlChanged"];
116
-
117
- NSString* js =
118
- @"(function(){"
119
- @" function emit(){"
120
- @" try{ window.webkit.messageHandlers.fitUrlChanged.postMessage(location.href); }catch(e){}"
121
- @" }"
122
- @" var _ps = history.pushState;"
123
- @" history.pushState = function(){ _ps.apply(this, arguments); emit(); };"
124
- @" var _rs = history.replaceState;"
125
- @" history.replaceState = function(){ _rs.apply(this, arguments); emit(); };"
126
- @" window.addEventListener('popstate', emit, true);"
127
- @" document.addEventListener('click', function(ev){"
128
- @" var a = ev.target && ev.target.closest ? ev.target.closest('a[href]') : null;"
129
- @" if (!a) return;"
130
- @" if (a.target === '_blank' || a.hasAttribute('download')) return;"
131
- @" setTimeout(emit, 0);" // lascia tempo alla SPA di aggiornare l’URL
132
- @" }, true);"
133
- @"})();";
134
-
135
- WKUserScript* us = [[WKUserScript alloc]
136
- initWithSource:js
137
- injectionTime:WKUserScriptInjectionTimeAtDocumentStart
138
- forMainFrameOnly:YES];
139
-
140
- [d->ucc addUserScript:us];
141
- cfg.userContentController = d->ucc;
142
-
143
- d->wk = [[WKWebView alloc] initWithFrame:nsParent.bounds configuration:cfg];
144
- d->wk.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
145
- [nsParent addSubview:d->wk];
146
-
147
- d->delegate = [WKNavDelegate new];
148
- d->delegate.owner = this;
149
- [d->wk setNavigationDelegate:d->delegate];
150
- }
151
-
152
- WKWebViewWidget::~WKWebViewWidget() {
153
- if (!d) return;
154
- if (d->ucc && d->msg) {
155
- @try { [d->ucc removeScriptMessageHandlerForName:@"fitUrlChanged"]; } @catch (...) {}
156
- }
157
- d->msg = nil;
158
-
159
- if (d->wk) { [d->wk removeFromSuperview]; d->wk = nil; }
160
- d->delegate = nil;
161
- d->ucc = nil;
162
-
163
- delete d; d = nullptr;
164
- }
165
-
166
- void WKWebViewWidget::showEvent(QShowEvent* e) { QWidget::showEvent(e); }
167
- void WKWebViewWidget::resizeEvent(QResizeEvent* e) { QWidget::resizeEvent(e); }
168
-
169
- QUrl WKWebViewWidget::url() const {
170
- if (!(d && d->wk)) return QUrl();
171
- NSURL* nsurl = d->wk.URL;
172
- if (!nsurl) return QUrl();
173
- const char* utf8 = nsurl.absoluteString.UTF8String;
174
- if (!utf8) return QUrl();
175
- return QUrl::fromEncoded(QByteArray(utf8));
176
- }
177
-
178
- void WKWebViewWidget::setUrl(const QUrl& u) {
179
- if (!(d && d->wk)) return;
180
- NSURL* nsurl = toNSURL(u);
181
- if (!nsurl) return;
182
- [d->wk loadRequest:[NSURLRequest requestWithURL:nsurl]];
183
- }
184
-
185
- void WKWebViewWidget::back() { if (d && d->wk && d->wk.canGoBack) [d->wk goBack]; }
186
- void WKWebViewWidget::forward() { if (d && d->wk && d->wk.canGoForward) [d->wk goForward]; }
187
- void WKWebViewWidget::stop() { if (d && d->wk) [d->wk stopLoading:nil]; }
188
- void WKWebViewWidget::reload() { if (d && d->wk) [d->wk reload]; }
189
- void WKWebViewWidget::evaluateJavaScript(const QString& script) {
190
- if (!d || !d->wk) return;
191
- NSString* s = [NSString stringWithUTF8String:script.toUtf8().constData()];
192
- [d->wk evaluateJavaScript:s completionHandler:^(id result, NSError* error){
193
- Q_UNUSED(result); Q_UNUSED(error);
194
- }];
195
- }