fit-webview-bridge 0.2.2a5__tar.gz → 0.2.3a2__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.
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/PKG-INFO +1 -1
- fit_webview_bridge-0.2.3a2/examples/macos/wkwebview_demo.py +131 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/pyproject.toml +1 -1
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/src/macos/WKWebViewWidget.mm +357 -11
- fit_webview_bridge-0.2.2a5/examples/macos/wkwebview_demo.py +0 -43
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/.github/workflows/wheels-macos.yml +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/.gitignore +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/.vscode/settings.json +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/CMakeLists.txt +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/README.md +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/bindings/pyside6/macos/CMakeLists.txt +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/bindings/pyside6/macos/typesystem_wkwebview.xml +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/fit_webview_bridge/__init__.py +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/src/macos/CMakeLists.txt +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/src/macos/DownloadInfo.h +0 -0
- {fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/src/macos/WKWebViewWidget.h +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
QLineEdit,
|
|
15
|
+
QMainWindow,
|
|
16
|
+
QPushButton,
|
|
17
|
+
QVBoxLayout,
|
|
18
|
+
QWidget,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# tentativo 1: pacchetto generato da shiboken (wkwebview)
|
|
22
|
+
try:
|
|
23
|
+
import wkwebview
|
|
24
|
+
|
|
25
|
+
WKWebViewWidget = wkwebview.WKWebViewWidget
|
|
26
|
+
except Exception:
|
|
27
|
+
# tentativo 2: modulo nativo diretto
|
|
28
|
+
from _wkwebview import WKWebViewWidget
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
HOME_URL = "https://github.com/fit-project"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Main(QMainWindow):
|
|
35
|
+
def __init__(self):
|
|
36
|
+
super().__init__()
|
|
37
|
+
|
|
38
|
+
central = QWidget(self)
|
|
39
|
+
root = QVBoxLayout(central)
|
|
40
|
+
self.setCentralWidget(central)
|
|
41
|
+
|
|
42
|
+
# --- toolbar: back/forward/home + address bar + go ---
|
|
43
|
+
bar = QHBoxLayout()
|
|
44
|
+
self.btnBack = QPushButton("◀︎ Back")
|
|
45
|
+
self.btnFwd = QPushButton("Forward ▶︎")
|
|
46
|
+
self.btnHome = QPushButton("🏠 Home")
|
|
47
|
+
|
|
48
|
+
self.address = QLineEdit() # ← barra indirizzi
|
|
49
|
+
self.address.setPlaceholderText("Digita un URL o una ricerca…")
|
|
50
|
+
self.btnGo = QPushButton("Go")
|
|
51
|
+
|
|
52
|
+
bar.addWidget(self.btnBack)
|
|
53
|
+
bar.addWidget(self.btnFwd)
|
|
54
|
+
bar.addWidget(self.btnHome)
|
|
55
|
+
bar.addWidget(self.address, 1) # ← occupa spazio elastico
|
|
56
|
+
bar.addWidget(self.btnGo)
|
|
57
|
+
root.addLayout(bar)
|
|
58
|
+
|
|
59
|
+
# --- webview ---
|
|
60
|
+
self.view = WKWebViewWidget()
|
|
61
|
+
root.addWidget(self.view)
|
|
62
|
+
|
|
63
|
+
# segnali base
|
|
64
|
+
self.view.titleChanged.connect(self.setWindowTitle)
|
|
65
|
+
self.view.loadProgress.connect(lambda p: print("progress:", p))
|
|
66
|
+
|
|
67
|
+
# abilita/disabilita i bottoni in base alla navigazione
|
|
68
|
+
self.btnBack.setEnabled(False)
|
|
69
|
+
self.btnFwd.setEnabled(False)
|
|
70
|
+
self.view.canGoBackChanged.connect(self.btnBack.setEnabled)
|
|
71
|
+
self.view.canGoForwardChanged.connect(self.btnFwd.setEnabled)
|
|
72
|
+
|
|
73
|
+
# azioni bottoni
|
|
74
|
+
self.btnBack.clicked.connect(self.view.back)
|
|
75
|
+
self.btnFwd.clicked.connect(self.view.forward)
|
|
76
|
+
self.btnHome.clicked.connect(lambda: self.view.setUrl(QUrl(HOME_URL)))
|
|
77
|
+
|
|
78
|
+
# --- address bar: invio / bottone Go ---
|
|
79
|
+
def navigate_from_address():
|
|
80
|
+
text = (self.address.text() or "").strip()
|
|
81
|
+
if not text:
|
|
82
|
+
return
|
|
83
|
+
url = QUrl.fromUserInput(text) # gestisce http/https, domini, file, ecc.
|
|
84
|
+
self.view.setUrl(url)
|
|
85
|
+
|
|
86
|
+
self.address.returnPressed.connect(navigate_from_address)
|
|
87
|
+
self.btnGo.clicked.connect(navigate_from_address)
|
|
88
|
+
|
|
89
|
+
# mantieni sincronizzata la barra con la URL corrente
|
|
90
|
+
self.view.urlChanged.connect(lambda u: self.address.setText(u.toString()))
|
|
91
|
+
|
|
92
|
+
# --- eventi download: print semplici ---
|
|
93
|
+
self.view.downloadStarted.connect(
|
|
94
|
+
lambda name, path: print(f"[download] started: name='{name}' path='{path}'")
|
|
95
|
+
)
|
|
96
|
+
self.view.downloadProgress.connect(
|
|
97
|
+
lambda done, total: print(
|
|
98
|
+
f"[download] progress: {done}/{total if total >= 0 else '?'}"
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
self.view.downloadFailed.connect(
|
|
102
|
+
lambda path, err: print(f"[download] FAILED: path='{path}' err='{err}'")
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def on_finished(info):
|
|
106
|
+
try:
|
|
107
|
+
fname = info.fileName() if hasattr(info, "fileName") else None
|
|
108
|
+
directory = info.directory() if hasattr(info, "directory") else None
|
|
109
|
+
url = info.url().toString() if hasattr(info, "url") else None
|
|
110
|
+
if fname or directory or url:
|
|
111
|
+
print(
|
|
112
|
+
f"[download] finished: file='{fname}' dir='{directory}' url='{url}'"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
print(f"[download] finished: {info}")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"[download] finished (inspect error: {e}): {info}")
|
|
118
|
+
|
|
119
|
+
self.view.downloadFinished.connect(on_finished)
|
|
120
|
+
|
|
121
|
+
# carica home e imposta barra
|
|
122
|
+
self.view.setUrl(QUrl(HOME_URL))
|
|
123
|
+
self.address.setText(HOME_URL)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
app = QApplication([])
|
|
128
|
+
m = Main()
|
|
129
|
+
m.resize(1200, 800)
|
|
130
|
+
m.show()
|
|
131
|
+
app.exec()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fit-webview-bridge"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3a2"
|
|
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,232 @@ 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 inline NSString* FITURLStr(NSURL *u) { return u ? u.absoluteString : @"(nil)"; }
|
|
77
|
+
|
|
78
|
+
static NSString* FIT_CurrentLang(void) {
|
|
79
|
+
NSString *lang = NSLocale.preferredLanguages.firstObject ?: @"en";
|
|
80
|
+
// normalizza es. "it-IT" -> "it"
|
|
81
|
+
NSRange dash = [lang rangeOfString:@"-"];
|
|
82
|
+
return (dash.location != NSNotFound) ? [lang substringToIndex:dash.location] : lang;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static NSString* FIT_T(NSString* key) {
|
|
86
|
+
static NSDictionary *en, *it;
|
|
87
|
+
static dispatch_once_t once;
|
|
88
|
+
dispatch_once(&once, ^{
|
|
89
|
+
en = @{
|
|
90
|
+
@"menu.openLink": @"Open link",
|
|
91
|
+
@"menu.copyLink": @"Copy link address",
|
|
92
|
+
@"menu.openImage": @"Open image",
|
|
93
|
+
@"menu.copyImageURL": @"Copy image URL",
|
|
94
|
+
@"menu.downloadImage":@"Download image…",
|
|
95
|
+
};
|
|
96
|
+
it = @{
|
|
97
|
+
@"menu.openLink": @"Apri link",
|
|
98
|
+
@"menu.copyLink": @"Copia indirizzo link",
|
|
99
|
+
@"menu.openImage": @"Apri immagine",
|
|
100
|
+
@"menu.copyImageURL": @"Copia URL immagine",
|
|
101
|
+
@"menu.downloadImage":@"Scarica immagine…",
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
NSString *lang = FIT_CurrentLang();
|
|
105
|
+
NSDictionary *tbl = [lang isEqualToString:@"it"] ? it : en;
|
|
106
|
+
return tbl[key] ?: en[key] ?: key;
|
|
107
|
+
}
|
|
108
|
+
|
|
40
109
|
@implementation FitUrlMsgHandler
|
|
110
|
+
|
|
111
|
+
// Helpers per sanity-check su tipi da payload
|
|
112
|
+
static inline NSString* FITStringOrNil(id obj) {
|
|
113
|
+
return [obj isKindOfClass:NSString.class] ? (NSString*)obj : nil;
|
|
114
|
+
}
|
|
115
|
+
static inline NSNumber* FITNumberOrNil(id obj) {
|
|
116
|
+
return [obj isKindOfClass:NSNumber.class] ? (NSNumber*)obj : nil;
|
|
117
|
+
}
|
|
118
|
+
|
|
41
119
|
- (void)userContentController:(WKUserContentController *)userContentController
|
|
42
|
-
didReceiveScriptMessage:(WKScriptMessage *)message
|
|
120
|
+
didReceiveScriptMessage:(WKScriptMessage *)message
|
|
121
|
+
{
|
|
43
122
|
if (!self.owner) return;
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
123
|
+
|
|
124
|
+
if ([message.name isEqualToString:@"fitUrlChanged"]) {
|
|
125
|
+
if (![message.body isKindOfClass:[NSString class]]) return;
|
|
126
|
+
QString s = QString::fromUtf8([(NSString*)message.body UTF8String]);
|
|
127
|
+
emit self.owner->urlChanged(QUrl::fromEncoded(s.toUtf8()));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if ([message.name isEqualToString:@"fitContextMenu"]) {
|
|
132
|
+
if (![message.body isKindOfClass:[NSDictionary class]]) return;
|
|
133
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
134
|
+
[self _fitShowContextMenuFromPayload:(NSDictionary*)message.body];
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
- (void)_fitShowContextMenuFromPayload:(NSDictionary*)payload
|
|
141
|
+
{
|
|
142
|
+
WKWebView* wv = self.webView;
|
|
143
|
+
if (!wv || !wv.window) return;
|
|
144
|
+
|
|
145
|
+
NSString *linkStr = FITStringOrNil(payload[@"link"]);
|
|
146
|
+
NSString *imgStr = FITStringOrNil(payload[@"image"]);
|
|
147
|
+
NSURL *linkURL = (linkStr.length ? [NSURL URLWithString:linkStr] : nil);
|
|
148
|
+
NSURL *imgURL = (imgStr.length ? [NSURL URLWithString:imgStr] : nil);
|
|
149
|
+
if (!linkURL && !imgURL) return;
|
|
150
|
+
|
|
151
|
+
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
|
|
152
|
+
|
|
153
|
+
if (linkURL) {
|
|
154
|
+
NSMenuItem *open = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.openLink")
|
|
155
|
+
action:@selector(_fitOpenLink:)
|
|
156
|
+
keyEquivalent:@""];
|
|
157
|
+
open.target = self; open.representedObject = @{@"url": linkURL};
|
|
158
|
+
[menu addItem:open];
|
|
159
|
+
|
|
160
|
+
NSMenuItem *copy = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.copyLink")
|
|
161
|
+
action:@selector(_fitCopyURL:)
|
|
162
|
+
keyEquivalent:@""];
|
|
163
|
+
copy.target = self; copy.representedObject = @{@"url": linkURL};
|
|
164
|
+
[menu addItem:copy];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (imgURL) {
|
|
168
|
+
NSMenuItem *openImg = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.openImage")
|
|
169
|
+
action:@selector(_fitOpenLink:)
|
|
170
|
+
keyEquivalent:@""];
|
|
171
|
+
openImg.target = self; openImg.representedObject = @{@"url": imgURL};
|
|
172
|
+
[menu addItem:openImg];
|
|
173
|
+
|
|
174
|
+
NSMenuItem *copyImg = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.copyImageURL")
|
|
175
|
+
action:@selector(_fitCopyURL:)
|
|
176
|
+
keyEquivalent:@""];
|
|
177
|
+
copyImg.target = self; copyImg.representedObject = @{@"url": imgURL};
|
|
178
|
+
[menu addItem:copyImg];
|
|
179
|
+
|
|
180
|
+
NSMenuItem *dlImg = [[NSMenuItem alloc] initWithTitle:FIT_T(@"menu.downloadImage")
|
|
181
|
+
action:@selector(_fitDownloadImage:)
|
|
182
|
+
keyEquivalent:@""];
|
|
183
|
+
dlImg.target = self;
|
|
184
|
+
dlImg.representedObject = @{@"url": imgURL};
|
|
185
|
+
[menu addItem:[NSMenuItem separatorItem]];
|
|
186
|
+
[menu addItem:dlImg];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
NSPoint mouseOnScreen = [NSEvent mouseLocation];
|
|
190
|
+
NSPoint inWindow = [wv.window convertPointFromScreen:mouseOnScreen];
|
|
191
|
+
NSPoint inView = [wv convertPoint:inWindow fromView:nil];
|
|
192
|
+
|
|
193
|
+
[menu popUpMenuPositioningItem:nil atLocation:inView inView:wv];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
- (void)_fitOpenLink:(NSMenuItem*)item {
|
|
197
|
+
NSURL *url = ((NSDictionary*)item.representedObject)[@"url"];
|
|
198
|
+
if (!url || !self.webView) return;
|
|
199
|
+
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
- (void)_fitCopyURL:(NSMenuItem*)item {
|
|
203
|
+
NSURL *url = ((NSDictionary*)item.representedObject)[@"url"];
|
|
204
|
+
if (!url) return;
|
|
205
|
+
NSPasteboard *pb = [NSPasteboard generalPasteboard];
|
|
206
|
+
[pb clearContents];
|
|
207
|
+
[pb setString:url.absoluteString forType:NSPasteboardTypeString];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Utility: crea nome unico in una cartella
|
|
211
|
+
static NSString* fit_uniquePath(NSString* baseDir, NSString* filename) {
|
|
212
|
+
NSString* fname = filename.length ? filename : @"download";
|
|
213
|
+
NSString* path = [baseDir stringByAppendingPathComponent:fname];
|
|
214
|
+
NSFileManager* fm = [NSFileManager defaultManager];
|
|
215
|
+
if (![fm fileExistsAtPath:path]) return path;
|
|
216
|
+
|
|
217
|
+
NSString* name = [fname stringByDeletingPathExtension];
|
|
218
|
+
NSString* ext = [fname pathExtension];
|
|
219
|
+
for (NSUInteger i = 1; i < 10000; ++i) {
|
|
220
|
+
NSString* cand = ext.length
|
|
221
|
+
? [NSString stringWithFormat:@"%@ (%lu).%@", name, (unsigned long)i, ext]
|
|
222
|
+
: [NSString stringWithFormat:@"%@ (%lu)", name, (unsigned long)i];
|
|
223
|
+
NSString* candPath = [baseDir stringByAppendingPathComponent:cand];
|
|
224
|
+
if (![fm fileExistsAtPath:candPath]) return candPath;
|
|
225
|
+
}
|
|
226
|
+
return path;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Scarica un URL (usato dall’azione immagine)
|
|
230
|
+
- (void)_fitDownloadURL:(NSURL *)url suggestedName:(NSString *)suggestedName {
|
|
231
|
+
if (!url || !self.owner) return;
|
|
232
|
+
|
|
233
|
+
// cartella destinazione da Qt
|
|
234
|
+
QString qdir = self.owner->downloadDirectory();
|
|
235
|
+
NSString *destDir = [NSString stringWithUTF8String:qdir.toUtf8().constData()];
|
|
236
|
+
if (!destDir.length) destDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Downloads"];
|
|
237
|
+
[[NSFileManager defaultManager] createDirectoryAtPath:destDir
|
|
238
|
+
withIntermediateDirectories:YES
|
|
239
|
+
attributes:nil error:nil];
|
|
240
|
+
|
|
241
|
+
// nome iniziale
|
|
242
|
+
NSString *fname = suggestedName.length ? suggestedName : (url.lastPathComponent.length ? url.lastPathComponent : @"download");
|
|
243
|
+
NSString *tmpTarget = fit_uniquePath(destDir, fname);
|
|
244
|
+
|
|
245
|
+
// segnala start (nome provvisorio)
|
|
246
|
+
fit_emit_downloadStarted(self.owner,
|
|
247
|
+
QString::fromUtf8([tmpTarget lastPathComponent].UTF8String),
|
|
248
|
+
QString::fromUtf8(tmpTarget.UTF8String));
|
|
249
|
+
|
|
250
|
+
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
251
|
+
NSURLSession *session = [NSURLSession sessionWithConfiguration:cfg];
|
|
252
|
+
NSURLSessionDownloadTask *task =
|
|
253
|
+
[session downloadTaskWithURL:url
|
|
254
|
+
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error)
|
|
255
|
+
{
|
|
256
|
+
if (error) {
|
|
257
|
+
fit_emit_downloadFailed(self.owner,
|
|
258
|
+
QString::fromUtf8(tmpTarget.UTF8String),
|
|
259
|
+
QString::fromUtf8(error.localizedDescription.UTF8String));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// usa il suggerimento del server se c’è
|
|
264
|
+
NSString *serverName = response.suggestedFilename.length ? response.suggestedFilename : [tmpTarget lastPathComponent];
|
|
265
|
+
NSString *finalPath = fit_uniquePath(destDir, serverName);
|
|
266
|
+
|
|
267
|
+
NSError *mvErr = nil;
|
|
268
|
+
[[NSFileManager defaultManager] moveItemAtURL:location
|
|
269
|
+
toURL:[NSURL fileURLWithPath:finalPath]
|
|
270
|
+
error:&mvErr];
|
|
271
|
+
if (mvErr) {
|
|
272
|
+
fit_emit_downloadFailed(self.owner,
|
|
273
|
+
QString::fromUtf8(finalPath.UTF8String),
|
|
274
|
+
QString::fromUtf8(mvErr.localizedDescription.UTF8String));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
QUrl qsrc = QUrl::fromEncoded(QByteArray(url.absoluteString.UTF8String));
|
|
279
|
+
fit_emit_downloadFinished(self.owner,
|
|
280
|
+
QString::fromUtf8([finalPath lastPathComponent].UTF8String),
|
|
281
|
+
QString::fromUtf8([finalPath stringByDeletingLastPathComponent].UTF8String),
|
|
282
|
+
qsrc);
|
|
283
|
+
}];
|
|
284
|
+
[task resume];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
- (void)_fitDownloadImage:(NSMenuItem*)item {
|
|
288
|
+
NSURL *url = ((NSDictionary*)item.representedObject)[@"url"];
|
|
289
|
+
[self _fitDownloadURL:url suggestedName:nil];
|
|
48
290
|
}
|
|
49
291
|
@end
|
|
50
292
|
|
|
293
|
+
|
|
51
294
|
// ===== WKNavDelegate =====
|
|
52
295
|
@interface WKNavDelegate : NSObject <WKNavigationDelegate, WKDownloadDelegate, WKUIDelegate>
|
|
53
296
|
@property(nonatomic, assign) WKWebViewWidget* owner;
|
|
@@ -58,6 +301,9 @@ static NSURL* toNSURL(QUrl u);
|
|
|
58
301
|
@property(nonatomic, strong) NSMapTable<WKDownload*, NSNumber*>* expectedTotals; // weak->strong
|
|
59
302
|
@property(nonatomic, strong) NSMapTable<WKDownload*, NSURL*>* sourceURLs; // weak->strong
|
|
60
303
|
@property(nonatomic, strong) NSMapTable<WKDownload*, NSString*>* suggestedNames; // weak->strong
|
|
304
|
+
@property(nonatomic, strong) NSURL* pendingPopupParentURL;
|
|
305
|
+
@property(nonatomic, strong) NSURL* pendingPopupChildURL;
|
|
306
|
+
@property(nonatomic, assign) WKWebView* webView;
|
|
61
307
|
@end
|
|
62
308
|
|
|
63
309
|
@implementation WKNavDelegate
|
|
@@ -75,17 +321,15 @@ static NSURL* toNSURL(QUrl u);
|
|
|
75
321
|
}
|
|
76
322
|
|
|
77
323
|
#pragma mark - Navigazione
|
|
78
|
-
|
|
79
|
-
|
|
80
324
|
// 1a) Navigation: intercetta click con targetFrame == nil (tipico di _blank)
|
|
81
325
|
- (void)webView:(WKWebView *)webView
|
|
82
326
|
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
|
|
83
327
|
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
|
|
84
328
|
{
|
|
85
|
-
// Se è un _blank, no-op qui: ci pensa createWebView... (sopra)
|
|
86
329
|
decisionHandler(WKNavigationActionPolicyAllow);
|
|
87
330
|
}
|
|
88
331
|
|
|
332
|
+
|
|
89
333
|
// 1b) UI: invocato quando la pagina chiede esplicitamente una nuova webview
|
|
90
334
|
- (WKWebView *)webView:(WKWebView *)webView
|
|
91
335
|
createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
|
|
@@ -93,8 +337,18 @@ createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
|
|
|
93
337
|
windowFeatures:(WKWindowFeatures *)windowFeatures
|
|
94
338
|
{
|
|
95
339
|
if (navigationAction.targetFrame == nil || !navigationAction.targetFrame.isMainFrame) {
|
|
96
|
-
|
|
340
|
+
NSURL *parent = webView.URL;
|
|
341
|
+
NSURL *child = navigationAction.request.URL;
|
|
342
|
+
|
|
343
|
+
// salva coppia padre/figlio per il “ritorno” post-download
|
|
344
|
+
self.pendingPopupParentURL = parent;
|
|
345
|
+
self.pendingPopupChildURL = child;
|
|
346
|
+
|
|
347
|
+
if (child) {
|
|
348
|
+
[webView loadRequest:navigationAction.request]; // apri nella stessa webview
|
|
349
|
+
}
|
|
97
350
|
}
|
|
351
|
+
|
|
98
352
|
return nil; // restituisci nil per NON creare una nuova finestra
|
|
99
353
|
}
|
|
100
354
|
|
|
@@ -149,10 +403,27 @@ createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
|
|
|
149
403
|
decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
|
|
150
404
|
decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
|
|
151
405
|
{
|
|
406
|
+
NSURLResponse *resp = navigationResponse.response;
|
|
407
|
+
NSURL *url = resp.URL;
|
|
408
|
+
|
|
409
|
+
BOOL isAttachment = NO;
|
|
410
|
+
if ([resp isKindOfClass:NSHTTPURLResponse.class]) {
|
|
411
|
+
NSHTTPURLResponse *http = (NSHTTPURLResponse *)resp;
|
|
412
|
+
NSString *cd = http.allHeaderFields[@"Content-Disposition"];
|
|
413
|
+
if (cd && [[cd lowercaseString] containsString:@"attachment"]) {
|
|
414
|
+
isAttachment = YES;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (isAttachment) {
|
|
419
|
+
decisionHandler(WKNavigationResponsePolicyDownload);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
152
423
|
if (navigationResponse.canShowMIMEType) {
|
|
153
424
|
decisionHandler(WKNavigationResponsePolicyAllow);
|
|
154
425
|
} else {
|
|
155
|
-
decisionHandler(WKNavigationResponsePolicyDownload);
|
|
426
|
+
decisionHandler(WKNavigationResponsePolicyDownload);
|
|
156
427
|
}
|
|
157
428
|
}
|
|
158
429
|
|
|
@@ -341,6 +612,42 @@ completionHandler:(void (^)(NSURL * _Nullable destination))completionHandler
|
|
|
341
612
|
DownloadInfo* info = new DownloadInfo(qFileName, qDir, qUrl, self.owner);
|
|
342
613
|
emit self.owner->downloadFinished(info);
|
|
343
614
|
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
WKWebView *webView = self.webView;
|
|
618
|
+
NSURL *srcURL = [self.sourceURLs objectForKey:download];
|
|
619
|
+
|
|
620
|
+
if (webView && self.pendingPopupChildURL && srcURL &&
|
|
621
|
+
[srcURL isEqual:self.pendingPopupChildURL]) {
|
|
622
|
+
|
|
623
|
+
WKBackForwardList *bf = webView.backForwardList;
|
|
624
|
+
NSURL *current = webView.URL;
|
|
625
|
+
NSURL *backURL = bf.backItem.URL;
|
|
626
|
+
|
|
627
|
+
// CASI:
|
|
628
|
+
// A) Sei sul FIGLIO → torna indietro alla PARENT
|
|
629
|
+
if (current && [current isEqual:self.pendingPopupChildURL]) {
|
|
630
|
+
[webView goBack];
|
|
631
|
+
}
|
|
632
|
+
// B) Sei già sulla PARENT → non fare nulla
|
|
633
|
+
else if (current && [current isEqual:self.pendingPopupParentURL]) {
|
|
634
|
+
// niente
|
|
635
|
+
}
|
|
636
|
+
// C) Non sei sul child, ma l’item precedente è la PARENT → goBack
|
|
637
|
+
else if (backURL && [backURL isEqual:self.pendingPopupParentURL]) {
|
|
638
|
+
[webView goBack];
|
|
639
|
+
}
|
|
640
|
+
// D) Fallback: carica esplicitamente la PARENT
|
|
641
|
+
else if (self.pendingPopupParentURL) {
|
|
642
|
+
[webView loadRequest:[NSURLRequest requestWithURL:self.pendingPopupParentURL]];
|
|
643
|
+
} else {
|
|
644
|
+
//Niente
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// pulizia stato
|
|
648
|
+
self.pendingPopupChildURL = nil;
|
|
649
|
+
self.pendingPopupParentURL = nil;
|
|
650
|
+
}
|
|
344
651
|
// 5) cleanup mappe
|
|
345
652
|
if (finalPath) [self.downloadPaths removeObjectForKey:download];
|
|
346
653
|
[self.progressToDownload removeObjectForKey:download.progress];
|
|
@@ -368,6 +675,20 @@ completionHandler:(void (^)(NSURL * _Nullable destination))completionHandler
|
|
|
368
675
|
QString::fromUtf8(error.localizedDescription.UTF8String)
|
|
369
676
|
);
|
|
370
677
|
|
|
678
|
+
// 🔙 Se il download proviene dal "figlio", torna alla "pagina padre"
|
|
679
|
+
WKWebView *webView = self.webView;
|
|
680
|
+
NSURL *src = [self.sourceURLs objectForKey:download];
|
|
681
|
+
if (webView && self.pendingPopupChildURL && src && [src isEqual:self.pendingPopupChildURL]) {
|
|
682
|
+
if (webView.canGoBack) {
|
|
683
|
+
[webView goBack];
|
|
684
|
+
} else if (self.pendingPopupParentURL) {
|
|
685
|
+
[webView loadRequest:[NSURLRequest requestWithURL:self.pendingPopupParentURL]];
|
|
686
|
+
}
|
|
687
|
+
// ripulisci lo stato
|
|
688
|
+
self.pendingPopupChildURL = nil;
|
|
689
|
+
self.pendingPopupParentURL = nil;
|
|
690
|
+
}
|
|
691
|
+
|
|
371
692
|
// cleanup mappe
|
|
372
693
|
if (finalPath) [self.downloadPaths removeObjectForKey:download];
|
|
373
694
|
[self.progressToDownload removeObjectForKey:download.progress];
|
|
@@ -415,6 +736,11 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
|
|
|
415
736
|
cfg.defaultWebpagePreferences.allowsContentJavaScript = YES;
|
|
416
737
|
}
|
|
417
738
|
|
|
739
|
+
// ✅ Consenti window.open() senza creare una nuova finestra UI
|
|
740
|
+
@try {
|
|
741
|
+
cfg.preferences.javaScriptCanOpenWindowsAutomatically = YES;
|
|
742
|
+
} @catch (...) {}
|
|
743
|
+
|
|
418
744
|
// --- Fullscreen HTML5 (via KVC tollerante) ---
|
|
419
745
|
@try {
|
|
420
746
|
[cfg.preferences setValue:@YES forKey:@"fullScreenEnabled"];
|
|
@@ -435,6 +761,7 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
|
|
|
435
761
|
d->msg = [FitUrlMsgHandler new];
|
|
436
762
|
d->msg.owner = this;
|
|
437
763
|
[d->ucc addScriptMessageHandler:d->msg name:@"fitUrlChanged"];
|
|
764
|
+
[d->ucc addScriptMessageHandler:d->msg name:@"fitContextMenu"];
|
|
438
765
|
|
|
439
766
|
NSString* js =
|
|
440
767
|
@"(function(){"
|
|
@@ -447,8 +774,25 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
|
|
|
447
774
|
@" if (!a) return; if (a.target === '_blank' || a.hasAttribute('download')) return;"
|
|
448
775
|
@" setTimeout(emit, 0);"
|
|
449
776
|
@" }, true);"
|
|
777
|
+
@"})();"
|
|
778
|
+
@"(function(){"
|
|
779
|
+
@" document.addEventListener('contextmenu', function(ev){"
|
|
780
|
+
@" var el = ev.target;"
|
|
781
|
+
@" var a = el && el.closest ? el.closest('a[href]') : null;"
|
|
782
|
+
@" var img = el && el.closest ? el.closest('img[src]') : null;"
|
|
783
|
+
@" if (!a && !img) return;" // lascia il menu nativo altrove
|
|
784
|
+
@" ev.preventDefault();"
|
|
785
|
+
@" try {"
|
|
786
|
+
@" window.webkit.messageHandlers.fitContextMenu.postMessage({"
|
|
787
|
+
@" x: ev.clientX, y: ev.clientY,"
|
|
788
|
+
@" link: a ? a.href : null,"
|
|
789
|
+
@" image: img ? img.src : null"
|
|
790
|
+
@" });"
|
|
791
|
+
@" } catch(e){}"
|
|
792
|
+
@" }, true);"
|
|
450
793
|
@"})();";
|
|
451
794
|
|
|
795
|
+
|
|
452
796
|
WKUserScript* us = [[WKUserScript alloc]
|
|
453
797
|
initWithSource:js
|
|
454
798
|
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
|
|
@@ -458,12 +802,13 @@ WKWebViewWidget::WKWebViewWidget(QWidget* parent)
|
|
|
458
802
|
|
|
459
803
|
d->wk = [[WKWebView alloc] initWithFrame:nsParent.bounds configuration:cfg];
|
|
460
804
|
d->wk.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
805
|
+
[d->msg setWebView:d->wk];
|
|
461
806
|
[nsParent addSubview:d->wk];
|
|
462
807
|
|
|
463
808
|
d->delegate = [WKNavDelegate new];
|
|
464
809
|
d->delegate.owner = this;
|
|
810
|
+
d->delegate.webView = d->wk;
|
|
465
811
|
[d->wk setNavigationDelegate:d->delegate];
|
|
466
|
-
|
|
467
812
|
[d->wk setUIDelegate:d->delegate];
|
|
468
813
|
}
|
|
469
814
|
|
|
@@ -472,6 +817,7 @@ WKWebViewWidget::~WKWebViewWidget() {
|
|
|
472
817
|
|
|
473
818
|
if (d->ucc && d->msg) {
|
|
474
819
|
@try { [d->ucc removeScriptMessageHandlerForName:@"fitUrlChanged"]; } @catch (...) {}
|
|
820
|
+
@try { [d->ucc removeScriptMessageHandlerForName:@"fitContextMenu"]; } @catch (...) {}
|
|
475
821
|
}
|
|
476
822
|
d->msg = nil;
|
|
477
823
|
|
|
@@ -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()
|
{fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/.github/workflows/wheels-macos.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fit_webview_bridge-0.2.2a5 → fit_webview_bridge-0.2.3a2}/bindings/pyside6/macos/CMakeLists.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|