plusui-native-core 0.1.45 → 0.1.48
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.
- package/Core/Features/Window/window.cpp +306 -3
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include <iostream>
|
|
4
4
|
#include <map>
|
|
5
5
|
#include <plusui/display.hpp>
|
|
6
|
+
#include <plusui/events.hpp>
|
|
6
7
|
#include <plusui/tray.hpp>
|
|
7
8
|
#include <plusui/webgpu.hpp>
|
|
8
9
|
#include <plusui/window.hpp>
|
|
@@ -51,6 +52,54 @@ constexpr char kHideScrollbarsScript[] = R"(
|
|
|
51
52
|
window.addEventListener("load", ensureStyle);
|
|
52
53
|
})();
|
|
53
54
|
)";
|
|
55
|
+
|
|
56
|
+
#ifdef _WIN32
|
|
57
|
+
static std::string jsonEscape(const std::string &input) {
|
|
58
|
+
std::string out;
|
|
59
|
+
out.reserve(input.size() + 8);
|
|
60
|
+
for (char c : input) {
|
|
61
|
+
switch (c) {
|
|
62
|
+
case '\\':
|
|
63
|
+
out += "\\\\";
|
|
64
|
+
break;
|
|
65
|
+
case '"':
|
|
66
|
+
out += "\\\"";
|
|
67
|
+
break;
|
|
68
|
+
case '\n':
|
|
69
|
+
out += "\\n";
|
|
70
|
+
break;
|
|
71
|
+
case '\r':
|
|
72
|
+
out += "\\r";
|
|
73
|
+
break;
|
|
74
|
+
case '\t':
|
|
75
|
+
out += "\\t";
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
out += c;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static std::string mimeTypeFromPath(const std::string &path) {
|
|
86
|
+
size_t dot = path.find_last_of('.');
|
|
87
|
+
if (dot == std::string::npos)
|
|
88
|
+
return "application/octet-stream";
|
|
89
|
+
std::string ext = path.substr(dot);
|
|
90
|
+
for (char &ch : ext)
|
|
91
|
+
ch = static_cast<char>(tolower(static_cast<unsigned char>(ch)));
|
|
92
|
+
if (ext == ".png") return "image/png";
|
|
93
|
+
if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg";
|
|
94
|
+
if (ext == ".gif") return "image/gif";
|
|
95
|
+
if (ext == ".webp") return "image/webp";
|
|
96
|
+
if (ext == ".svg") return "image/svg+xml";
|
|
97
|
+
if (ext == ".txt") return "text/plain";
|
|
98
|
+
if (ext == ".json") return "application/json";
|
|
99
|
+
if (ext == ".pdf") return "application/pdf";
|
|
100
|
+
return "application/octet-stream";
|
|
101
|
+
}
|
|
102
|
+
#endif
|
|
54
103
|
} // namespace
|
|
55
104
|
|
|
56
105
|
struct Window::Impl {
|
|
@@ -93,6 +142,7 @@ struct Window::Impl {
|
|
|
93
142
|
WNDPROC originalProc = nullptr;
|
|
94
143
|
ComPtr<ICoreWebView2Controller> controller;
|
|
95
144
|
ComPtr<ICoreWebView2> webview;
|
|
145
|
+
static std::map<HWND, Impl *> embeddedWebviewByParent;
|
|
96
146
|
|
|
97
147
|
static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
98
148
|
Impl *impl = nullptr;
|
|
@@ -128,6 +178,113 @@ struct Window::Impl {
|
|
|
128
178
|
for (auto &cb : impl->closeCallbacks)
|
|
129
179
|
cb();
|
|
130
180
|
break;
|
|
181
|
+
case WM_DROPFILES: {
|
|
182
|
+
HDROP hDrop = reinterpret_cast<HDROP>(wp);
|
|
183
|
+
Impl *targetImpl = impl;
|
|
184
|
+
if (!targetImpl->webview) {
|
|
185
|
+
auto it = embeddedWebviewByParent.find(hwnd);
|
|
186
|
+
if (it != embeddedWebviewByParent.end()) {
|
|
187
|
+
targetImpl = it->second;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!targetImpl || !targetImpl->config.enableFileDrop ||
|
|
192
|
+
!targetImpl->webview) {
|
|
193
|
+
DragFinish(hDrop);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
|
|
198
|
+
std::string filesJson = "[";
|
|
199
|
+
|
|
200
|
+
for (UINT i = 0; i < fileCount; ++i) {
|
|
201
|
+
UINT pathLen = DragQueryFileW(hDrop, i, nullptr, 0);
|
|
202
|
+
std::wstring wpath(pathLen + 1, L'\0');
|
|
203
|
+
DragQueryFileW(hDrop, i, &wpath[0], pathLen + 1);
|
|
204
|
+
wpath.resize(pathLen);
|
|
205
|
+
|
|
206
|
+
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1,
|
|
207
|
+
nullptr, 0, nullptr, nullptr);
|
|
208
|
+
std::string path;
|
|
209
|
+
if (utf8Len > 0) {
|
|
210
|
+
path.resize(static_cast<size_t>(utf8Len - 1));
|
|
211
|
+
WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, &path[0],
|
|
212
|
+
utf8Len, nullptr, nullptr);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
std::string name = path;
|
|
216
|
+
size_t lastSlash = path.find_last_of("\\/");
|
|
217
|
+
if (lastSlash != std::string::npos) {
|
|
218
|
+
name = path.substr(lastSlash + 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
unsigned long long sizeBytes = 0;
|
|
222
|
+
WIN32_FILE_ATTRIBUTE_DATA fileAttr;
|
|
223
|
+
if (GetFileAttributesExW(wpath.c_str(), GetFileExInfoStandard,
|
|
224
|
+
&fileAttr)) {
|
|
225
|
+
ULARGE_INTEGER size;
|
|
226
|
+
size.HighPart = fileAttr.nFileSizeHigh;
|
|
227
|
+
size.LowPart = fileAttr.nFileSizeLow;
|
|
228
|
+
sizeBytes = size.QuadPart;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (i > 0)
|
|
232
|
+
filesJson += ",";
|
|
233
|
+
filesJson +=
|
|
234
|
+
"{\"path\":\"" + jsonEscape(path) + "\",\"name\":\"" +
|
|
235
|
+
jsonEscape(name) + "\",\"type\":\"" +
|
|
236
|
+
jsonEscape(mimeTypeFromPath(path)) + "\",\"size\":" +
|
|
237
|
+
std::to_string(sizeBytes) + "}";
|
|
238
|
+
}
|
|
239
|
+
filesJson += "]";
|
|
240
|
+
|
|
241
|
+
DragFinish(hDrop);
|
|
242
|
+
|
|
243
|
+
POINT dropPoint = {0, 0};
|
|
244
|
+
DragQueryPoint(hDrop, &dropPoint);
|
|
245
|
+
|
|
246
|
+
std::string zoneHitScript =
|
|
247
|
+
"(function(){try{"
|
|
248
|
+
"var el=document.elementFromPoint(" +
|
|
249
|
+
std::to_string(dropPoint.x) + "," + std::to_string(dropPoint.y) +
|
|
250
|
+
");"
|
|
251
|
+
"if(!el) return false;"
|
|
252
|
+
"if(!el.closest) return false;"
|
|
253
|
+
"return !!el.closest('[data-dropzone=\\\"true\\\"], .filedrop-zone, .dropzone');"
|
|
254
|
+
"}catch(_){return false;}})();";
|
|
255
|
+
|
|
256
|
+
targetImpl->webview->ExecuteScript(
|
|
257
|
+
std::wstring(zoneHitScript.begin(), zoneHitScript.end()).c_str(),
|
|
258
|
+
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
|
|
259
|
+
[targetImpl, filesJson](HRESULT errorCode,
|
|
260
|
+
LPCWSTR resultJson) -> HRESULT {
|
|
261
|
+
if (FAILED(errorCode) || !targetImpl || !targetImpl->webview)
|
|
262
|
+
return S_OK;
|
|
263
|
+
|
|
264
|
+
bool overDropZone = false;
|
|
265
|
+
if (resultJson) {
|
|
266
|
+
std::wstring wres(resultJson);
|
|
267
|
+
overDropZone = (wres.find(L"true") != std::wstring::npos);
|
|
268
|
+
}
|
|
269
|
+
if (!overDropZone)
|
|
270
|
+
return S_OK;
|
|
271
|
+
|
|
272
|
+
std::string eventScript =
|
|
273
|
+
"window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped',"
|
|
274
|
+
" { detail: { files: " +
|
|
275
|
+
filesJson + " } }));";
|
|
276
|
+
|
|
277
|
+
event::emit("fileDrop.filesDropped", filesJson);
|
|
278
|
+
|
|
279
|
+
targetImpl->webview->ExecuteScript(
|
|
280
|
+
std::wstring(eventScript.begin(), eventScript.end())
|
|
281
|
+
.c_str(),
|
|
282
|
+
nullptr);
|
|
283
|
+
return S_OK;
|
|
284
|
+
})
|
|
285
|
+
.Get());
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
131
288
|
case WM_SETFOCUS:
|
|
132
289
|
impl->state.isFocused = true;
|
|
133
290
|
for (auto &cb : impl->focusCallbacks)
|
|
@@ -151,6 +308,10 @@ struct Window::Impl {
|
|
|
151
308
|
|
|
152
309
|
Window::Window() : pImpl(std::shared_ptr<Impl>(new Impl())) {}
|
|
153
310
|
|
|
311
|
+
#ifdef _WIN32
|
|
312
|
+
std::map<HWND, Window::Impl *> Window::Impl::embeddedWebviewByParent;
|
|
313
|
+
#endif
|
|
314
|
+
|
|
154
315
|
Window::~Window() = default;
|
|
155
316
|
|
|
156
317
|
Window::Window(Window &&other) noexcept = default;
|
|
@@ -207,6 +368,8 @@ Window Window::create(const WindowConfig &config) {
|
|
|
207
368
|
w.pImpl->state.width = config.width;
|
|
208
369
|
w.pImpl->state.height = config.height;
|
|
209
370
|
|
|
371
|
+
DragAcceptFiles(w.pImpl->hwnd, config.enableFileDrop ? TRUE : FALSE);
|
|
372
|
+
|
|
210
373
|
if (config.center) {
|
|
211
374
|
RECT screen;
|
|
212
375
|
SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
|
|
@@ -801,6 +964,20 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
801
964
|
controller->get_ParentWindow(&parentHwnd);
|
|
802
965
|
GetClientRect(parentHwnd, &bounds);
|
|
803
966
|
controller->put_Bounds(bounds);
|
|
967
|
+
Window::Impl::embeddedWebviewByParent[parentHwnd] =
|
|
968
|
+
pImpl.get();
|
|
969
|
+
|
|
970
|
+
ComPtr<ICoreWebView2Controller4> controller4;
|
|
971
|
+
if (controller &&
|
|
972
|
+
SUCCEEDED(controller->QueryInterface(
|
|
973
|
+
IID_PPV_ARGS(&controller4))) &&
|
|
974
|
+
controller4) {
|
|
975
|
+
bool allowExternalDrop =
|
|
976
|
+
!(pImpl->config.enableFileDrop ||
|
|
977
|
+
pImpl->config.disableWebviewDragDrop);
|
|
978
|
+
controller4->put_AllowExternalDrop(
|
|
979
|
+
allowExternalDrop ? TRUE : FALSE);
|
|
980
|
+
}
|
|
804
981
|
|
|
805
982
|
pImpl->nativeWebView = pImpl->webview.Get();
|
|
806
983
|
pImpl->ready = true;
|
|
@@ -839,15 +1016,41 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
839
1016
|
if (window.__plusui_nativeFileDropOnly) return;
|
|
840
1017
|
window.__plusui_nativeFileDropOnly = true;
|
|
841
1018
|
|
|
1019
|
+
var isOverDropZone = function(e) {
|
|
1020
|
+
if (!e) return false;
|
|
1021
|
+
var target = null;
|
|
1022
|
+
if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) {
|
|
1023
|
+
target = document.elementFromPoint(e.clientX, e.clientY);
|
|
1024
|
+
}
|
|
1025
|
+
if (!target && e.target && e.target.nodeType === 1) {
|
|
1026
|
+
target = e.target;
|
|
1027
|
+
}
|
|
1028
|
+
if (!target || !target.closest) return false;
|
|
1029
|
+
return !!target.closest('[data-dropzone="true"], .filedrop-zone, .dropzone');
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
var zoneActive = false;
|
|
1033
|
+
var emitZoneState = function(next) {
|
|
1034
|
+
if (next === zoneActive) return;
|
|
1035
|
+
zoneActive = next;
|
|
1036
|
+
window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));
|
|
1037
|
+
};
|
|
1038
|
+
|
|
842
1039
|
var block = function(e) {
|
|
843
1040
|
if (!e) return false;
|
|
1041
|
+
var overDropZone = isOverDropZone(e);
|
|
1042
|
+
if (e.type === 'drop') {
|
|
1043
|
+
emitZoneState(false);
|
|
1044
|
+
} else {
|
|
1045
|
+
emitZoneState(overDropZone);
|
|
1046
|
+
}
|
|
844
1047
|
e.preventDefault();
|
|
845
1048
|
e.stopPropagation();
|
|
846
1049
|
if (typeof e.stopImmediatePropagation === 'function') {
|
|
847
1050
|
e.stopImmediatePropagation();
|
|
848
1051
|
}
|
|
849
1052
|
if (e.dataTransfer) {
|
|
850
|
-
try { e.dataTransfer.dropEffect = 'none'; } catch (_) {}
|
|
1053
|
+
try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {}
|
|
851
1054
|
}
|
|
852
1055
|
return false;
|
|
853
1056
|
};
|
|
@@ -1117,6 +1320,64 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1117
1320
|
result = canForward ? "true" : "false";
|
|
1118
1321
|
success = true;
|
|
1119
1322
|
}
|
|
1323
|
+
} else if (method.find("fileDrop.") == 0) {
|
|
1324
|
+
std::string fileDropMethod = method.substr(9);
|
|
1325
|
+
if (fileDropMethod == "setEnabled") {
|
|
1326
|
+
size_t p1 = msg.find("[");
|
|
1327
|
+
size_t p2 = msg.find("]");
|
|
1328
|
+
if (p1 != std::string::npos &&
|
|
1329
|
+
p2 != std::string::npos) {
|
|
1330
|
+
std::string params =
|
|
1331
|
+
msg.substr(p1 + 1, p2 - p1 - 1);
|
|
1332
|
+
bool enabled =
|
|
1333
|
+
params.find("true") !=
|
|
1334
|
+
std::string::npos;
|
|
1335
|
+
pImpl->config.enableFileDrop = enabled;
|
|
1336
|
+
|
|
1337
|
+
HWND targetHwnd = nullptr;
|
|
1338
|
+
if (pImpl->window) {
|
|
1339
|
+
targetHwnd = static_cast<HWND>(
|
|
1340
|
+
pImpl->window->nativeHandle());
|
|
1341
|
+
}
|
|
1342
|
+
if (!targetHwnd && pImpl->controller) {
|
|
1343
|
+
pImpl->controller->get_ParentWindow(
|
|
1344
|
+
&targetHwnd);
|
|
1345
|
+
}
|
|
1346
|
+
if (targetHwnd) {
|
|
1347
|
+
DragAcceptFiles(targetHwnd,
|
|
1348
|
+
enabled ? TRUE
|
|
1349
|
+
: FALSE);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (pImpl->controller) {
|
|
1353
|
+
ComPtr<ICoreWebView2Controller4>
|
|
1354
|
+
controller4;
|
|
1355
|
+
if (SUCCEEDED(pImpl->controller.As(
|
|
1356
|
+
&controller4)) &&
|
|
1357
|
+
controller4) {
|
|
1358
|
+
bool allowExternalDrop =
|
|
1359
|
+
!(pImpl->config.enableFileDrop ||
|
|
1360
|
+
pImpl->config
|
|
1361
|
+
.disableWebviewDragDrop);
|
|
1362
|
+
controller4->put_AllowExternalDrop(
|
|
1363
|
+
allowExternalDrop ? TRUE
|
|
1364
|
+
: FALSE);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
success = true;
|
|
1369
|
+
} else if (fileDropMethod == "isEnabled") {
|
|
1370
|
+
result = pImpl->config.enableFileDrop
|
|
1371
|
+
? "true"
|
|
1372
|
+
: "false";
|
|
1373
|
+
success = true;
|
|
1374
|
+
} else if (fileDropMethod == "startDrag") {
|
|
1375
|
+
result = "false";
|
|
1376
|
+
success = true;
|
|
1377
|
+
} else if (fileDropMethod ==
|
|
1378
|
+
"clearCallbacks") {
|
|
1379
|
+
success = true;
|
|
1380
|
+
}
|
|
1120
1381
|
}
|
|
1121
1382
|
|
|
1122
1383
|
// Send response back to JS (matches
|
|
@@ -1190,12 +1451,28 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1190
1451
|
NSString *disableDragDropScript = @"(function() {"
|
|
1191
1452
|
"if (window.__plusui_nativeFileDropOnly) return;"
|
|
1192
1453
|
"window.__plusui_nativeFileDropOnly = true;"
|
|
1454
|
+
"var isOverDropZone = function(e) {"
|
|
1455
|
+
"if (!e) return false;"
|
|
1456
|
+
"var target = null;"
|
|
1457
|
+
"if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) { target = document.elementFromPoint(e.clientX, e.clientY); }"
|
|
1458
|
+
"if (!target && e.target && e.target.nodeType === 1) { target = e.target; }"
|
|
1459
|
+
"if (!target || !target.closest) return false;"
|
|
1460
|
+
"return !!target.closest('[data-dropzone=\\\"true\\\"], .filedrop-zone, .dropzone');"
|
|
1461
|
+
"};"
|
|
1462
|
+
"var zoneActive = false;"
|
|
1463
|
+
"var emitZoneState = function(next) {"
|
|
1464
|
+
"if (next === zoneActive) return;"
|
|
1465
|
+
"zoneActive = next;"
|
|
1466
|
+
"window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));"
|
|
1467
|
+
"};"
|
|
1193
1468
|
"var block = function(e) {"
|
|
1194
1469
|
"if (!e) return false;"
|
|
1470
|
+
"var overDropZone = isOverDropZone(e);"
|
|
1471
|
+
"if (e.type === 'drop') { emitZoneState(false); } else { emitZoneState(overDropZone); }"
|
|
1195
1472
|
"e.preventDefault();"
|
|
1196
1473
|
"e.stopPropagation();"
|
|
1197
1474
|
"if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();"
|
|
1198
|
-
"if (e.dataTransfer) { try { e.dataTransfer.dropEffect = 'none'; } catch (_) {} }"
|
|
1475
|
+
"if (e.dataTransfer) { try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {} }"
|
|
1199
1476
|
"return false;"
|
|
1200
1477
|
"};"
|
|
1201
1478
|
"window.__plusui_dragDropEvents = ['dragenter','dragover','dragleave','drop'];"
|
|
@@ -1670,15 +1947,41 @@ void Window::setWebviewDragDropEnabled(bool enabled) {
|
|
|
1670
1947
|
window.__plusui_nativeFileDropOnly = true;
|
|
1671
1948
|
window.__plusui_dragDropDisabled = true;
|
|
1672
1949
|
|
|
1950
|
+
var isOverDropZone = function(e) {
|
|
1951
|
+
if (!e) return false;
|
|
1952
|
+
var target = null;
|
|
1953
|
+
if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) {
|
|
1954
|
+
target = document.elementFromPoint(e.clientX, e.clientY);
|
|
1955
|
+
}
|
|
1956
|
+
if (!target && e.target && e.target.nodeType === 1) {
|
|
1957
|
+
target = e.target;
|
|
1958
|
+
}
|
|
1959
|
+
if (!target || !target.closest) return false;
|
|
1960
|
+
return !!target.closest('[data-dropzone="true"], .filedrop-zone, .dropzone');
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
var zoneActive = false;
|
|
1964
|
+
var emitZoneState = function(next) {
|
|
1965
|
+
if (next === zoneActive) return;
|
|
1966
|
+
zoneActive = next;
|
|
1967
|
+
window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1673
1970
|
var block = function(e) {
|
|
1674
1971
|
if (!e) return false;
|
|
1972
|
+
var overDropZone = isOverDropZone(e);
|
|
1973
|
+
if (e.type === 'drop') {
|
|
1974
|
+
emitZoneState(false);
|
|
1975
|
+
} else {
|
|
1976
|
+
emitZoneState(overDropZone);
|
|
1977
|
+
}
|
|
1675
1978
|
e.preventDefault();
|
|
1676
1979
|
e.stopPropagation();
|
|
1677
1980
|
if (typeof e.stopImmediatePropagation === 'function') {
|
|
1678
1981
|
e.stopImmediatePropagation();
|
|
1679
1982
|
}
|
|
1680
1983
|
if (e.dataTransfer) {
|
|
1681
|
-
try { e.dataTransfer.dropEffect = 'none'; } catch (_) {}
|
|
1984
|
+
try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {}
|
|
1682
1985
|
}
|
|
1683
1986
|
return false;
|
|
1684
1987
|
};
|