fit-webview-bridge 0.2.2a4__tar.gz → 0.2.3a1__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.2a4
3
+ Version: 0.2.3a1
4
4
  Summary: Qt native WebView bridge with PySide6 bindings
5
5
  Author: FIT Project
6
6
  License: LGPL-3.0-or-later
@@ -0,0 +1,110 @@
1
+ import os
2
+ import sys
3
+
4
+ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
5
+ sys.path[:0] = [
6
+ os.path.join(ROOT, "build"),
7
+ os.path.join(ROOT, "build", "bindings", "shiboken_out"),
8
+ ]
9
+
10
+ from PySide6.QtCore import QUrl
11
+ from PySide6.QtWidgets import (
12
+ QApplication,
13
+ QHBoxLayout,
14
+ QMainWindow,
15
+ QPushButton,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ # tentativo 1: pacchetto generato da shiboken (wkwebview)
21
+ try:
22
+ import wkwebview
23
+
24
+ WKWebViewWidget = wkwebview.WKWebViewWidget
25
+ except Exception:
26
+ # tentativo 2: modulo nativo diretto
27
+ from _wkwebview import WKWebViewWidget
28
+
29
+
30
+ HOME_URL = "https://google.it"
31
+
32
+
33
+ class Main(QMainWindow):
34
+ def __init__(self):
35
+ super().__init__()
36
+
37
+ central = QWidget(self)
38
+ root = QVBoxLayout(central)
39
+ self.setCentralWidget(central)
40
+
41
+ # --- toolbar semplice ---
42
+ bar = QHBoxLayout()
43
+ self.btnBack = QPushButton("◀︎ Back")
44
+ self.btnFwd = QPushButton("Forward ▶︎")
45
+ self.btnHome = QPushButton("🏠 Home")
46
+ bar.addWidget(self.btnBack)
47
+ bar.addWidget(self.btnFwd)
48
+ bar.addWidget(self.btnHome)
49
+ bar.addStretch(1)
50
+ root.addLayout(bar)
51
+
52
+ # --- webview ---
53
+ self.view = WKWebViewWidget()
54
+ root.addWidget(self.view)
55
+
56
+ # segnali base
57
+ self.view.titleChanged.connect(self.setWindowTitle)
58
+ self.view.loadProgress.connect(lambda p: print("progress:", p))
59
+
60
+ # abilita/disabilita i bottoni in base alla navigazione
61
+ self.btnBack.setEnabled(False)
62
+ self.btnFwd.setEnabled(False)
63
+ self.view.canGoBackChanged.connect(self.btnBack.setEnabled)
64
+ self.view.canGoForwardChanged.connect(self.btnFwd.setEnabled)
65
+
66
+ # azioni bottoni
67
+ self.btnBack.clicked.connect(self.view.back)
68
+ self.btnFwd.clicked.connect(self.view.forward)
69
+ self.btnHome.clicked.connect(lambda: self.view.setUrl(QUrl(HOME_URL)))
70
+
71
+ # --- eventi download: print semplici ---
72
+ self.view.downloadStarted.connect(
73
+ lambda name, path: print(f"[download] started: name='{name}' path='{path}'")
74
+ )
75
+ self.view.downloadProgress.connect(
76
+ lambda done, total: print(
77
+ f"[download] progress: {done}/{total if total >= 0 else '?'}"
78
+ )
79
+ )
80
+ self.view.downloadFailed.connect(
81
+ lambda path, err: print(f"[download] FAILED: path='{path}' err='{err}'")
82
+ )
83
+
84
+ def on_finished(info):
85
+ # Proviamo a leggere i getter se disponibili; fallback a repr
86
+ try:
87
+ fname = info.fileName() if hasattr(info, "fileName") else None
88
+ directory = info.directory() if hasattr(info, "directory") else None
89
+ url = info.url().toString() if hasattr(info, "url") else None
90
+ if fname or directory or url:
91
+ print(
92
+ f"[download] finished: file='{fname}' dir='{directory}' url='{url}'"
93
+ )
94
+ else:
95
+ print(f"[download] finished: {info}")
96
+ except Exception as e:
97
+ print(f"[download] finished (inspect error: {e}): {info}")
98
+
99
+ self.view.downloadFinished.connect(on_finished)
100
+
101
+ # carica home
102
+ self.view.setUrl(QUrl(HOME_URL))
103
+
104
+
105
+ if __name__ == "__main__":
106
+ app = QApplication([])
107
+ m = Main()
108
+ m.resize(1200, 800)
109
+ m.show()
110
+ app.exec()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fit-webview-bridge"
3
- version = "0.2.2a4"
3
+ version = "0.2.3a1"
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"]
@@ -6,6 +6,36 @@
6
6
  #include "DownloadInfo.h"
7
7
 
8
8
 
9
+ static inline void fit_emit_downloadStarted(WKWebViewWidget* owner,
10
+ const QString& name,
11
+ const QString& path) {
12
+ if (!owner) return;
13
+ QMetaObject::invokeMethod(owner, [owner, name, path]{
14
+ emit owner->downloadStarted(name, path);
15
+ }, Qt::QueuedConnection);
16
+ }
17
+
18
+ static inline void fit_emit_downloadFailed(WKWebViewWidget* owner,
19
+ const QString& path,
20
+ const QString& err) {
21
+ if (!owner) return;
22
+ QMetaObject::invokeMethod(owner, [owner, path, err]{
23
+ emit owner->downloadFailed(path, err);
24
+ }, Qt::QueuedConnection);
25
+ }
26
+
27
+ static inline void fit_emit_downloadFinished(WKWebViewWidget* owner,
28
+ const QString& fileName,
29
+ const QString& dir,
30
+ const QUrl& src) {
31
+ if (!owner) return;
32
+ QMetaObject::invokeMethod(owner, [owner, fileName, dir, src]{
33
+ auto *info = new DownloadInfo(fileName, dir, src, owner);
34
+ emit owner->downloadFinished(info);
35
+ }, Qt::QueuedConnection);
36
+ }
37
+
38
+
9
39
  #include <QtWidgets>
10
40
  #include <QString>
11
41
  #include <QUrl>
@@ -35,19 +65,230 @@ static NSURL* toNSURL(QUrl u);
35
65
  // =======================
36
66
  @interface FitUrlMsgHandler : NSObject <WKScriptMessageHandler>
37
67
  @property(nonatomic, assign) WKWebViewWidget* owner;
68
+ @property(nonatomic, assign) WKWebView* webView;
69
+
70
+ - (void)_fitShowContextMenuFromPayload:(NSDictionary*)payload;
71
+ - (void)_fitOpenLink:(NSMenuItem*)item;
72
+ - (void)_fitCopyURL:(NSMenuItem*)item;
38
73
  @end
39
74
 
75
+
76
+ static NSString* FIT_CurrentLang(void) {
77
+ NSString *lang = NSLocale.preferredLanguages.firstObject ?: @"en";
78
+ // normalizza es. "it-IT" -> "it"
79
+ NSRange dash = [lang rangeOfString:@"-"];
80
+ return (dash.location != NSNotFound) ? [lang substringToIndex:dash.location] : lang;
81
+ }
82
+
83
+ static NSString* FIT_T(NSString* key) {
84
+ static NSDictionary *en, *it;
85
+ static dispatch_once_t once;
86
+ dispatch_once(&once, ^{
87
+ en = @{
88
+ @"menu.openLink": @"Open link",
89
+ @"menu.copyLink": @"Copy link address",
90
+ @"menu.openImage": @"Open image",
91
+ @"menu.copyImageURL": @"Copy image URL",
92
+ @"menu.downloadImage":@"Download image…",
93
+ };
94
+ it = @{
95
+ @"menu.openLink": @"Apri link",
96
+ @"menu.copyLink": @"Copia indirizzo link",
97
+ @"menu.openImage": @"Apri immagine",
98
+ @"menu.copyImageURL": @"Copia URL immagine",
99
+ @"menu.downloadImage":@"Scarica immagine…",
100
+ };
101
+ });
102
+ NSString *lang = FIT_CurrentLang();
103
+ NSDictionary *tbl = [lang isEqualToString:@"it"] ? it : en;
104
+ return tbl[key] ?: en[key] ?: key;
105
+ }
106
+
40
107
  @implementation FitUrlMsgHandler
108
+
109
+ // Helpers per sanity-check su tipi da payload
110
+ static inline NSString* FITStringOrNil(id obj) {
111
+ return [obj isKindOfClass:NSString.class] ? (NSString*)obj : nil;
112
+ }
113
+ static inline NSNumber* FITNumberOrNil(id obj) {
114
+ return [obj isKindOfClass:NSNumber.class] ? (NSNumber*)obj : nil;
115
+ }
116
+
41
117
  - (void)userContentController:(WKUserContentController *)userContentController
42
- didReceiveScriptMessage:(WKScriptMessage *)message {
118
+ didReceiveScriptMessage:(WKScriptMessage *)message
119
+ {
43
120
  if (!self.owner) return;
44
- if (![message.name isEqualToString:@"fitUrlChanged"]) return;
45
- if (![message.body isKindOfClass:[NSString class]]) return;
46
- QString s = QString::fromUtf8([(NSString*)message.body UTF8String]);
47
- emit self.owner->urlChanged(QUrl::fromEncoded(s.toUtf8()));
121
+
122
+ if ([message.name isEqualToString:@"fitUrlChanged"]) {
123
+ if (![message.body isKindOfClass:[NSString class]]) return;
124
+ QString s = QString::fromUtf8([(NSString*)message.body UTF8String]);
125
+ emit self.owner->urlChanged(QUrl::fromEncoded(s.toUtf8()));
126
+ return;
127
+ }
128
+
129
+ if ([message.name isEqualToString:@"fitContextMenu"]) {
130
+ if (![message.body isKindOfClass:[NSDictionary class]]) return;
131
+ dispatch_async(dispatch_get_main_queue(), ^{
132
+ [self _fitShowContextMenuFromPayload:(NSDictionary*)message.body];
133
+ });
134
+ return;
135
+ }
136
+ }
137
+
138
+ - (void)_fitShowContextMenuFromPayload:(NSDictionary*)payload
139
+ {
140
+ WKWebView* wv = self.webView;
141
+ if (!wv || !wv.window) return;
142
+
143
+ NSString *linkStr = FITStringOrNil(payload[@"link"]);
144
+ NSString *imgStr = FITStringOrNil(payload[@"image"]);
145
+ NSURL *linkURL = (linkStr.length ? [NSURL URLWithString:linkStr] : nil);
146
+ NSURL *imgURL = (imgStr.length ? [NSURL URLWithString:imgStr] : nil);
147
+ if (!linkURL && !imgURL) return;
148
+
149
+ NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
150
+
151
+ if (linkURL) {
152
+ NSMenuItem *open = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.openLink")
153
+ action:@selector(_fitOpenLink:)
154
+ keyEquivalent:@""];
155
+ open.target = self; open.representedObject = @{@"url": linkURL};
156
+ [menu addItem:open];
157
+
158
+ NSMenuItem *copy = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.copyLink")
159
+ action:@selector(_fitCopyURL:)
160
+ keyEquivalent:@""];
161
+ copy.target = self; copy.representedObject = @{@"url": linkURL};
162
+ [menu addItem:copy];
163
+ }
164
+
165
+ if (imgURL) {
166
+ NSMenuItem *openImg = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.openImage")
167
+ action:@selector(_fitOpenLink:)
168
+ keyEquivalent:@""];
169
+ openImg.target = self; openImg.representedObject = @{@"url": imgURL};
170
+ [menu addItem:openImg];
171
+
172
+ NSMenuItem *copyImg = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.copyImageURL")
173
+ action:@selector(_fitCopyURL:)
174
+ keyEquivalent:@""];
175
+ copyImg.target = self; copyImg.representedObject = @{@"url": imgURL};
176
+ [menu addItem:copyImg];
177
+
178
+ NSMenuItem *dlImg = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.downloadImage")
179
+ action:@selector(_fitDownloadImage:)
180
+ keyEquivalent:@""];
181
+ dlImg.target = self;
182
+ dlImg.representedObject = @{@"url": imgURL};
183
+ [menu addItem:[NSMenuItem separatorItem]];
184
+ [menu addItem:dlImg];
185
+ }
186
+
187
+ NSPoint mouseOnScreen = [NSEvent mouseLocation];
188
+ NSPoint inWindow = [wv.window convertPointFromScreen:mouseOnScreen];
189
+ NSPoint inView = [wv convertPoint:inWindow fromView:nil];
190
+
191
+ [menu popUpMenuPositioningItem:nil atLocation:inView inView:wv];
192
+ }
193
+
194
+ - (void)_fitOpenLink:(NSMenuItem*)item {
195
+ NSURL *url = ((NSDictionary*)item.representedObject)[@"url"];
196
+ if (!url || !self.webView) return;
197
+ [self.webView loadRequest:[NSURLRequest requestWithURL:url]];
198
+ }
199
+
200
+ - (void)_fitCopyURL:(NSMenuItem*)item {
201
+ NSURL *url = ((NSDictionary*)item.representedObject)[@"url"];
202
+ if (!url) return;
203
+ NSPasteboard *pb = [NSPasteboard generalPasteboard];
204
+ [pb clearContents];
205
+ [pb setString:url.absoluteString forType:NSPasteboardTypeString];
206
+ }
207
+
208
+ // Utility: crea nome unico in una cartella
209
+ static NSString* fit_uniquePath(NSString* baseDir, NSString* filename) {
210
+ NSString* fname = filename.length ? filename : @"download";
211
+ NSString* path = [baseDir stringByAppendingPathComponent:fname];
212
+ NSFileManager* fm = [NSFileManager defaultManager];
213
+ if (![fm fileExistsAtPath:path]) return path;
214
+
215
+ NSString* name = [fname stringByDeletingPathExtension];
216
+ NSString* ext = [fname pathExtension];
217
+ for (NSUInteger i = 1; i < 10000; ++i) {
218
+ NSString* cand = ext.length
219
+ ? [NSString stringWithFormat:@"%@ (%lu).%@", name, (unsigned long)i, ext]
220
+ : [NSString stringWithFormat:@"%@ (%lu)", name, (unsigned long)i];
221
+ NSString* candPath = [baseDir stringByAppendingPathComponent:cand];
222
+ if (![fm fileExistsAtPath:candPath]) return candPath;
223
+ }
224
+ return path;
225
+ }
226
+
227
+ // Scarica un URL (usato dall’azione immagine)
228
+ - (void)_fitDownloadURL:(NSURL *)url suggestedName:(NSString *)suggestedName {
229
+ if (!url || !self.owner) return;
230
+
231
+ // cartella destinazione da Qt
232
+ QString qdir = self.owner->downloadDirectory();
233
+ NSString *destDir = [NSString stringWithUTF8String:qdir.toUtf8().constData()];
234
+ if (!destDir.length) destDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Downloads"];
235
+ [[NSFileManager defaultManager] createDirectoryAtPath:destDir
236
+ withIntermediateDirectories:YES
237
+ attributes:nil error:nil];
238
+
239
+ // nome iniziale
240
+ NSString *fname = suggestedName.length ? suggestedName : (url.lastPathComponent.length ? url.lastPathComponent : @"download");
241
+ NSString *tmpTarget = fit_uniquePath(destDir, fname);
242
+
243
+ // segnala start (nome provvisorio)
244
+ fit_emit_downloadStarted(self.owner,
245
+ QString::fromUtf8([tmpTarget lastPathComponent].UTF8String),
246
+ QString::fromUtf8(tmpTarget.UTF8String));
247
+
248
+ NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
249
+ NSURLSession *session = [NSURLSession sessionWithConfiguration:cfg];
250
+ NSURLSessionDownloadTask *task =
251
+ [session downloadTaskWithURL:url
252
+ completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error)
253
+ {
254
+ if (error) {
255
+ fit_emit_downloadFailed(self.owner,
256
+ QString::fromUtf8(tmpTarget.UTF8String),
257
+ QString::fromUtf8(error.localizedDescription.UTF8String));
258
+ return;
259
+ }
260
+
261
+ // usa il suggerimento del server se c’è
262
+ NSString *serverName = response.suggestedFilename.length ? response.suggestedFilename : [tmpTarget lastPathComponent];
263
+ NSString *finalPath = fit_uniquePath(destDir, serverName);
264
+
265
+ NSError *mvErr = nil;
266
+ [[NSFileManager defaultManager] moveItemAtURL:location
267
+ toURL:[NSURL fileURLWithPath:finalPath]
268
+ error:&mvErr];
269
+ if (mvErr) {
270
+ fit_emit_downloadFailed(self.owner,
271
+ QString::fromUtf8(finalPath.UTF8String),
272
+ QString::fromUtf8(mvErr.localizedDescription.UTF8String));
273
+ return;
274
+ }
275
+
276
+ QUrl qsrc = QUrl::fromEncoded(QByteArray(url.absoluteString.UTF8String));
277
+ fit_emit_downloadFinished(self.owner,
278
+ QString::fromUtf8([finalPath lastPathComponent].UTF8String),
279
+ QString::fromUtf8([finalPath stringByDeletingLastPathComponent].UTF8String),
280
+ qsrc);
281
+ }];
282
+ [task resume];
283
+ }
284
+
285
+ - (void)_fitDownloadImage:(NSMenuItem*)item {
286
+ NSURL *url = ((NSDictionary*)item.representedObject)[@"url"];
287
+ [self _fitDownloadURL:url suggestedName:nil];
48
288
  }
49
289
  @end
50
290
 
291
+
51
292
  // ===== WKNavDelegate =====
52
293
  @interface WKNavDelegate : NSObject <WKNavigationDelegate, WKDownloadDelegate, WKUIDelegate>
53
294
  @property(nonatomic, assign) WKWebViewWidget* owner;
@@ -82,12 +323,7 @@ static NSURL* toNSURL(QUrl u);
82
323
  decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
83
324
  decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
84
325
  {
85
- // targetFrame == nil => richiesta per nuova finestra (target="_blank" o window.open)
86
- if (navigationAction.targetFrame == nil || !navigationAction.targetFrame.isMainFrame) {
87
- [webView loadRequest:navigationAction.request]; // carica QUI
88
- decisionHandler(WKNavigationActionPolicyCancel); // cancella la creazione nuova finestra
89
- return;
90
- }
326
+ // Se è un _blank, no-op qui: ci pensa createWebView... (sopra)
91
327
  decisionHandler(WKNavigationActionPolicyAllow);
92
328
  }
93
329
 
@@ -440,6 +676,7 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
440
676
  d->msg = [FitUrlMsgHandler new];
441
677
  d->msg.owner = this;
442
678
  [d->ucc addScriptMessageHandler:d->msg name:@"fitUrlChanged"];
679
+ [d->ucc addScriptMessageHandler:d->msg name:@"fitContextMenu"];
443
680
 
444
681
  NSString* js =
445
682
  @"(function(){"
@@ -452,8 +689,25 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
452
689
  @" if (!a) return; if (a.target === '_blank' || a.hasAttribute('download')) return;"
453
690
  @" setTimeout(emit, 0);"
454
691
  @" }, true);"
692
+ @"})();"
693
+ @"(function(){"
694
+ @" document.addEventListener('contextmenu', function(ev){"
695
+ @" var el = ev.target;"
696
+ @" var a = el && el.closest ? el.closest('a[href]') : null;"
697
+ @" var img = el && el.closest ? el.closest('img[src]') : null;"
698
+ @" if (!a && !img) return;" // lascia il menu nativo altrove
699
+ @" ev.preventDefault();"
700
+ @" try {"
701
+ @" window.webkit.messageHandlers.fitContextMenu.postMessage({"
702
+ @" x: ev.clientX, y: ev.clientY,"
703
+ @" link: a ? a.href : null,"
704
+ @" image: img ? img.src : null"
705
+ @" });"
706
+ @" } catch(e){}"
707
+ @" }, true);"
455
708
  @"})();";
456
709
 
710
+
457
711
  WKUserScript* us = [[WKUserScript alloc]
458
712
  initWithSource:js
459
713
  injectionTime:WKUserScriptInjectionTimeAtDocumentStart
@@ -463,6 +717,7 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
463
717
 
464
718
  d->wk = [[WKWebView alloc] initWithFrame:nsParent.bounds configuration:cfg];
465
719
  d->wk.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
720
+ [d->msg setWebView:d->wk];
466
721
  [nsParent addSubview:d->wk];
467
722
 
468
723
  d->delegate = [WKNavDelegate new];
@@ -477,6 +732,7 @@ WKWebViewWidget::~WKWebViewWidget() {
477
732
 
478
733
  if (d->ucc && d->msg) {
479
734
  @try { [d->ucc removeScriptMessageHandlerForName:@"fitUrlChanged"]; } @catch (...) {}
735
+ @try { [d->ucc removeScriptMessageHandlerForName:@"fitContextMenu"]; } @catch (...) {}
480
736
  }
481
737
  d->msg = nil;
482
738
 
@@ -1,43 +0,0 @@
1
- import os
2
- import sys
3
-
4
- ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
5
- sys.path[:0] = [
6
- os.path.join(ROOT, "build"),
7
- os.path.join(ROOT, "build", "bindings", "shiboken_out"),
8
- ]
9
-
10
- from PySide6.QtCore import QUrl
11
- from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
12
-
13
- # tentativo 1: pacchetto generato da shiboken (wkwebview)
14
- try:
15
- import wkwebview
16
-
17
- WKWebViewWidget = wkwebview.WKWebViewWidget # accesso via attributo
18
- except Exception:
19
- # tentativo 2: modulo nativo diretto
20
- from _wkwebview import WKWebViewWidget
21
-
22
-
23
- class Main(QMainWindow):
24
- def __init__(self):
25
- super().__init__()
26
- central = QWidget(self)
27
- lay = QVBoxLayout(central)
28
- self.view = WKWebViewWidget()
29
- lay.addWidget(self.view)
30
- self.setCentralWidget(central)
31
-
32
- # segnali utili
33
- self.view.titleChanged.connect(self.setWindowTitle)
34
- self.view.loadProgress.connect(lambda p: print("progress:", p))
35
-
36
- self.view.setUrl(QUrl("http://fit-project.org/"))
37
-
38
-
39
- app = QApplication([])
40
- m = Main()
41
- m.resize(1200, 800)
42
- m.show()
43
- app.exec()