fit-webview-bridge 0.2.2a5__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.2a5
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.2a5"
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;
@@ -435,6 +676,7 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
435
676
  d->msg = [FitUrlMsgHandler new];
436
677
  d->msg.owner = this;
437
678
  [d->ucc addScriptMessageHandler:d->msg name:@"fitUrlChanged"];
679
+ [d->ucc addScriptMessageHandler:d->msg name:@"fitContextMenu"];
438
680
 
439
681
  NSString* js =
440
682
  @"(function(){"
@@ -447,8 +689,25 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
447
689
  @" if (!a) return; if (a.target === '_blank' || a.hasAttribute('download')) return;"
448
690
  @" setTimeout(emit, 0);"
449
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);"
450
708
  @"})();";
451
709
 
710
+
452
711
  WKUserScript* us = [[WKUserScript alloc]
453
712
  initWithSource:js
454
713
  injectionTime:WKUserScriptInjectionTimeAtDocumentStart
@@ -458,6 +717,7 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
458
717
 
459
718
  d->wk = [[WKWebView alloc] initWithFrame:nsParent.bounds configuration:cfg];
460
719
  d->wk.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
720
+ [d->msg setWebView:d->wk];
461
721
  [nsParent addSubview:d->wk];
462
722
 
463
723
  d->delegate = [WKNavDelegate new];
@@ -472,6 +732,7 @@ WKWebViewWidget::~WKWebViewWidget() {
472
732
 
473
733
  if (d->ucc && d->msg) {
474
734
  @try { [d->ucc removeScriptMessageHandlerForName:@"fitUrlChanged"]; } @catch (...) {}
735
+ @try { [d->ucc removeScriptMessageHandlerForName:@"fitContextMenu"]; } @catch (...) {}
475
736
  }
476
737
  d->msg = nil;
477
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("https://web.whatsapp.com/"))
37
-
38
-
39
- app = QApplication([])
40
- m = Main()
41
- m.resize(1200, 800)
42
- m.show()
43
- app.exec()