fit-webview-bridge 0.2.0a4__tar.gz → 0.2.1a1__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.1a1
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.1a1"
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,386 @@
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
+ // Progress via NSProgress (best effort): alcuni siti non lo popolano
137
+ [download.progress addObserver:self
138
+ forKeyPath:@"fractionCompleted"
139
+ options:NSKeyValueObservingOptionNew
140
+ context:NULL];
141
+ }
142
+
143
+ - (void)webView:(WKWebView *)webView
144
+ navigationResponse:(WKNavigationResponse *)navigationResponse
145
+ didBecomeDownload:(WKDownload *)download
146
+ {
147
+ download.delegate = self;
148
+
149
+ NSString* suggested = navigationResponse.response.suggestedFilename ?: @"download";
150
+ if (self.owner) {
151
+ QString destDir = self.owner->downloadDirectory();
152
+ QString destPath = destDir + "/" + QString::fromUtf8(suggested.UTF8String);
153
+ emit self.owner->downloadStarted(QString::fromUtf8(suggested.UTF8String),
154
+ destPath);
155
+ }
156
+ [download.progress addObserver:self
157
+ forKeyPath:@"fractionCompleted"
158
+ options:NSKeyValueObservingOptionNew
159
+ context:NULL];
160
+ }
161
+
162
+ #pragma mark - Scegli destinazione
163
+
164
+ static NSString* uniquePath(NSString* baseDir, NSString* filename) {
165
+ NSString* fname = filename ?: @"download";
166
+ NSString* path = [baseDir stringByAppendingPathComponent:fname];
167
+ NSFileManager* fm = [NSFileManager defaultManager];
168
+ if (![fm fileExistsAtPath:path]) return path;
169
+
170
+ NSString* name = [fname stringByDeletingPathExtension];
171
+ NSString* ext = [fname pathExtension];
172
+ for (NSUInteger i = 1; i < 10000; ++i) {
173
+ NSString* cand = ext.length
174
+ ? [NSString stringWithFormat:@"%@ (%lu).%@", name, (unsigned long)i, ext]
175
+ : [NSString stringWithFormat:@"%@ (%lu)", name, (unsigned long)i];
176
+ NSString* candPath = [baseDir stringByAppendingPathComponent:cand];
177
+ if (![fm fileExistsAtPath:candPath]) return candPath;
178
+ }
179
+ return path;
180
+ }
181
+
182
+ - (void)download:(WKDownload *)download
183
+ decideDestinationUsingResponse:(NSURLResponse *)response
184
+ suggestedFilename:(NSString *)suggestedFilename
185
+ completionHandler:(void (^)(NSURL * _Nullable destination))completionHandler
186
+ {
187
+ if (!self.owner) { completionHandler(nil); return; }
188
+
189
+ QString qdir = self.owner->downloadDirectory();
190
+ NSString* dir = [NSString stringWithUTF8String:qdir.toUtf8().constData()];
191
+ if (!dir.length) {
192
+ dir = [NSHomeDirectory() stringByAppendingPathComponent:@"Downloads"];
193
+ }
194
+
195
+ [[NSFileManager defaultManager] createDirectoryAtPath:dir
196
+ withIntermediateDirectories:YES
197
+ attributes:nil error:nil];
198
+
199
+ NSString* finalPath = uniquePath(dir, suggestedFilename ?: @"download");
200
+ [self.downloadPaths setObject:finalPath forKey:download];
201
+
202
+ emit self.owner->downloadStarted(
203
+ QString::fromUtf8((suggestedFilename ?: @"download").UTF8String),
204
+ QString::fromUtf8(finalPath.UTF8String)
205
+ );
206
+
207
+ completionHandler([NSURL fileURLWithPath:finalPath]);
208
+ }
209
+
210
+ #pragma mark - Progress / Fine / Errore
211
+
212
+ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)obj
213
+ change:(NSDictionary *)change context:(void *)ctx
214
+ {
215
+ if (![keyPath isEqualToString:@"fractionCompleted"]) {
216
+ [super observeValueForKeyPath:keyPath ofObject:obj change:change context:ctx];
217
+ return;
218
+ }
219
+ if (!self.owner) return;
220
+
221
+ NSProgress* prog = (NSProgress*)obj;
222
+ int64_t total = prog.totalUnitCount; // può essere -1 (sconosciuto)
223
+ int64_t done = prog.completedUnitCount;
224
+ emit self.owner->downloadProgress(done, total >= 0 ? total : -1);
225
+ }
226
+
227
+ - (void)downloadDidFinish:(WKDownload *)download {
228
+ if (!self.owner) return;
229
+
230
+ @try { [download.progress removeObserver:self forKeyPath:@"fractionCompleted"]; } @catch (...) {}
231
+
232
+ NSString* finalPath = [self.downloadPaths objectForKey:download];
233
+ if (finalPath) {
234
+ emit self.owner->downloadFinished(QString::fromUtf8(finalPath.UTF8String));
235
+ [self.downloadPaths removeObjectForKey:download];
236
+ } else {
237
+ emit self.owner->downloadFinished(QString());
238
+ }
239
+ }
240
+
241
+ - (void)download:(WKDownload *)download didFailWithError:(NSError *)error resumeData:(NSData *)resumeData {
242
+ if (!self.owner) return;
243
+
244
+ @try { [download.progress removeObserver:self forKeyPath:@"fractionCompleted"]; } @catch (...) {}
245
+
246
+ NSString* finalPath = [self.downloadPaths objectForKey:download];
247
+ QString qpath = finalPath ? QString::fromUtf8(finalPath.UTF8String) : QString();
248
+ emit self.owner->downloadFailed(qpath, QString::fromUtf8(error.localizedDescription.UTF8String));
249
+ if (finalPath) [self.downloadPaths removeObjectForKey:download];
250
+ }
251
+
252
+ @end
253
+
254
+ // =======================
255
+ // QUrl -> NSURL (normalizza e forza https)
256
+ // =======================
257
+ static NSURL* toNSURL(QUrl u) {
258
+ if (!u.isValid()) return nil;
259
+
260
+ if (u.scheme().isEmpty())
261
+ u = QUrl::fromUserInput(u.toString());
262
+
263
+ // Forza sempre http -> https (nessuna eccezione)
264
+ if (u.scheme() == "http")
265
+ u.setScheme("https");
266
+
267
+ if (u.isLocalFile())
268
+ return [NSURL fileURLWithPath:[NSString stringWithUTF8String:u.toLocalFile().toUtf8().constData()]];
269
+
270
+ const QByteArray enc = u.toString(QUrl::FullyEncoded).toUtf8();
271
+ return [NSURL URLWithString:[NSString stringWithUTF8String:enc.constData()]];
272
+ }
273
+
274
+ // =======================
275
+ // WKWebViewWidget
276
+ // =======================
277
+ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
278
+ : QWidget(parent), d(new Impl) {
279
+ setAttribute(Qt::WA_NativeWindow, true);
280
+ (void)winId();
281
+
282
+ d->downloadDir = QDir::homePath() + "/Downloads";
283
+
284
+ NSView* nsParent = (__bridge NSView*)reinterpret_cast<void*>(winId());
285
+ WKWebViewConfiguration* cfg = [[WKWebViewConfiguration alloc] init];
286
+ if ([cfg respondsToSelector:@selector(defaultWebpagePreferences)]) {
287
+ cfg.defaultWebpagePreferences.allowsContentJavaScript = YES;
288
+ }
289
+
290
+ // SPA: intercetta pushState/replaceState/popstate/click
291
+ d->ucc = [WKUserContentController new];
292
+ d->msg = [FitUrlMsgHandler new];
293
+ d->msg.owner = this;
294
+ [d->ucc addScriptMessageHandler:d->msg name:@"fitUrlChanged"];
295
+
296
+ NSString* js =
297
+ @"(function(){"
298
+ @" function emit(){ try{ window.webkit.messageHandlers.fitUrlChanged.postMessage(location.href); }catch(e){} }"
299
+ @" var _ps = history.pushState; history.pushState = function(){ _ps.apply(this, arguments); emit(); };"
300
+ @" var _rs = history.replaceState; history.replaceState = function(){ _rs.apply(this, arguments); emit(); };"
301
+ @" window.addEventListener('popstate', emit, true);"
302
+ @" document.addEventListener('click', function(ev){"
303
+ @" var a = ev.target && ev.target.closest ? ev.target.closest('a[href]') : null;"
304
+ @" if (!a) return; if (a.target === '_blank' || a.hasAttribute('download')) return;"
305
+ @" setTimeout(emit, 0);"
306
+ @" }, true);"
307
+ @"})();";
308
+
309
+ WKUserScript* us = [[WKUserScript alloc]
310
+ initWithSource:js
311
+ injectionTime:WKUserScriptInjectionTimeAtDocumentStart
312
+ forMainFrameOnly:YES];
313
+ [d->ucc addUserScript:us];
314
+ cfg.userContentController = d->ucc;
315
+
316
+ d->wk = [[WKWebView alloc] initWithFrame:nsParent.bounds configuration:cfg];
317
+ d->wk.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
318
+ [nsParent addSubview:d->wk];
319
+
320
+ d->delegate = [WKNavDelegate new];
321
+ d->delegate.owner = this;
322
+ [d->wk setNavigationDelegate:d->delegate];
323
+ }
324
+
325
+ WKWebViewWidget::~WKWebViewWidget() {
326
+ if (!d) return;
327
+
328
+ if (d->ucc && d->msg) {
329
+ @try { [d->ucc removeScriptMessageHandlerForName:@"fitUrlChanged"]; } @catch (...) {}
330
+ }
331
+ d->msg = nil;
332
+
333
+ if (d->wk) { [d->wk removeFromSuperview]; d->wk = nil; }
334
+ d->delegate = nil;
335
+ d->ucc = nil;
336
+
337
+ delete d; d = nullptr;
338
+ }
339
+
340
+ void WKWebViewWidget::showEvent(QShowEvent* e) { QWidget::showEvent(e); }
341
+ void WKWebViewWidget::resizeEvent(QResizeEvent* e) { QWidget::resizeEvent(e); }
342
+
343
+ QUrl WKWebViewWidget::url() const {
344
+ if (!(d && d->wk)) return QUrl();
345
+ NSURL* nsurl = d->wk.URL;
346
+ if (!nsurl) return QUrl();
347
+ const char* utf8 = nsurl.absoluteString.UTF8String;
348
+ if (!utf8) return QUrl();
349
+ return QUrl::fromEncoded(QByteArray(utf8));
350
+ }
351
+
352
+ void WKWebViewWidget::setUrl(const QUrl& u) {
353
+ if (!(d && d->wk)) return;
354
+ NSURL* nsurl = toNSURL(u);
355
+ if (!nsurl) return;
356
+ [d->wk loadRequest:[NSURLRequest requestWithURL:nsurl]];
357
+ }
358
+
359
+ void WKWebViewWidget::back() { if (d && d->wk && d->wk.canGoBack) [d->wk goBack]; }
360
+ void WKWebViewWidget::forward() { if (d && d->wk && d->wk.canGoForward) [d->wk goForward]; }
361
+ void WKWebViewWidget::stop() { if (d && d->wk) [d->wk stopLoading:nil]; }
362
+ void WKWebViewWidget::reload() { if (d && d->wk) [d->wk reload]; }
363
+
364
+ void WKWebViewWidget::evaluateJavaScript(const QString& script) {
365
+ if (!d || !d->wk) return;
366
+ NSString* s = [NSString stringWithUTF8String:script.toUtf8().constData()];
367
+ [d->wk evaluateJavaScript:s completionHandler:^(id result, NSError* error){
368
+ Q_UNUSED(result); Q_UNUSED(error);
369
+ }];
370
+ }
371
+
372
+ // =======================
373
+ // Download directory API
374
+ // =======================
375
+ QString WKWebViewWidget::downloadDirectory() const {
376
+ return d ? d->downloadDir : QString();
377
+ }
378
+
379
+ void WKWebViewWidget::setDownloadDirectory(const QString& dirPath) {
380
+ if (!d) return;
381
+ QString p = QDir::fromNativeSeparators(dirPath);
382
+ if (p.endsWith('/')) p.chop(1);
383
+ if (p.isEmpty()) return;
384
+ QDir().mkpath(p);
385
+ d->downloadDir = p;
386
+ }
@@ -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
- }