plusui-native-core 0.1.44 → 0.1.47
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.
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
#include <mutex>
|
|
4
4
|
#include <filesystem>
|
|
5
5
|
#include <fstream>
|
|
6
|
+
#include <unordered_map>
|
|
7
|
+
#include <cctype>
|
|
6
8
|
|
|
7
9
|
#ifdef _WIN32
|
|
8
10
|
#define WIN32_LEAN_AND_MEAN
|
|
@@ -171,8 +173,10 @@ static std::string getMimeType(const std::string& filePath) {
|
|
|
171
173
|
// Platform-specific helper to handle dropped files
|
|
172
174
|
#ifdef _WIN32
|
|
173
175
|
// Windows implementation for handling WM_DROPFILES message
|
|
174
|
-
void HandleDropFiles(
|
|
175
|
-
|
|
176
|
+
void HandleDropFiles(bool enabled,
|
|
177
|
+
const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
|
|
178
|
+
HDROP hDrop) {
|
|
179
|
+
if (!enabled || !filesDroppedCallback) {
|
|
176
180
|
return;
|
|
177
181
|
}
|
|
178
182
|
|
|
@@ -215,8 +219,8 @@ void HandleDropFiles(FileDrop::Impl* impl, HDROP hDrop) {
|
|
|
215
219
|
|
|
216
220
|
DragFinish(hDrop);
|
|
217
221
|
|
|
218
|
-
if (!files.empty() &&
|
|
219
|
-
|
|
222
|
+
if (!files.empty() && filesDroppedCallback) {
|
|
223
|
+
filesDroppedCallback(files);
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
#endif
|
|
@@ -225,8 +229,10 @@ void HandleDropFiles(FileDrop::Impl* impl, HDROP hDrop) {
|
|
|
225
229
|
// macOS implementation using NSPasteboard and drag&drop APIs
|
|
226
230
|
// This would be implemented with Objective-C++ integration with NSView
|
|
227
231
|
// For now, providing the interface structure
|
|
228
|
-
void HandleMacDropFiles(
|
|
229
|
-
|
|
232
|
+
void HandleMacDropFiles(bool enabled,
|
|
233
|
+
const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
|
|
234
|
+
NSArray<NSURL*>* urls) {
|
|
235
|
+
if (!enabled || !filesDroppedCallback) {
|
|
230
236
|
return;
|
|
231
237
|
}
|
|
232
238
|
|
|
@@ -256,16 +262,18 @@ void HandleMacDropFiles(FileDrop::Impl* impl, NSArray<NSURL*>* urls) {
|
|
|
256
262
|
files.push_back(info);
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
if (!files.empty() &&
|
|
260
|
-
|
|
265
|
+
if (!files.empty() && filesDroppedCallback) {
|
|
266
|
+
filesDroppedCallback(files);
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
#endif
|
|
264
270
|
|
|
265
271
|
#ifdef __linux__
|
|
266
272
|
// Linux/GTK implementation
|
|
267
|
-
void HandleGtkDropFiles(
|
|
268
|
-
|
|
273
|
+
void HandleGtkDropFiles(bool enabled,
|
|
274
|
+
const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
|
|
275
|
+
GtkSelectionData* data) {
|
|
276
|
+
if (!enabled || !filesDroppedCallback) {
|
|
269
277
|
return;
|
|
270
278
|
}
|
|
271
279
|
|
|
@@ -307,8 +315,8 @@ void HandleGtkDropFiles(FileDrop::Impl* impl, GtkSelectionData* data) {
|
|
|
307
315
|
g_strfreev(uris);
|
|
308
316
|
}
|
|
309
317
|
|
|
310
|
-
if (!files.empty() &&
|
|
311
|
-
|
|
318
|
+
if (!files.empty() && filesDroppedCallback) {
|
|
319
|
+
filesDroppedCallback(files);
|
|
312
320
|
}
|
|
313
321
|
}
|
|
314
322
|
#endif
|
|
@@ -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,81 @@ 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
|
+
std::string eventScript =
|
|
244
|
+
"window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped',"
|
|
245
|
+
" { detail: { files: " +
|
|
246
|
+
filesJson + " } }));";
|
|
247
|
+
|
|
248
|
+
// Also emit to backend event API so C++ listeners can react.
|
|
249
|
+
event::emit("fileDrop.filesDropped", filesJson);
|
|
250
|
+
|
|
251
|
+
targetImpl->webview->ExecuteScript(
|
|
252
|
+
std::wstring(eventScript.begin(), eventScript.end()).c_str(),
|
|
253
|
+
nullptr);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
131
256
|
case WM_SETFOCUS:
|
|
132
257
|
impl->state.isFocused = true;
|
|
133
258
|
for (auto &cb : impl->focusCallbacks)
|
|
@@ -151,6 +276,10 @@ struct Window::Impl {
|
|
|
151
276
|
|
|
152
277
|
Window::Window() : pImpl(std::shared_ptr<Impl>(new Impl())) {}
|
|
153
278
|
|
|
279
|
+
#ifdef _WIN32
|
|
280
|
+
std::map<HWND, Window::Impl *> Window::Impl::embeddedWebviewByParent;
|
|
281
|
+
#endif
|
|
282
|
+
|
|
154
283
|
Window::~Window() = default;
|
|
155
284
|
|
|
156
285
|
Window::Window(Window &&other) noexcept = default;
|
|
@@ -207,6 +336,8 @@ Window Window::create(const WindowConfig &config) {
|
|
|
207
336
|
w.pImpl->state.width = config.width;
|
|
208
337
|
w.pImpl->state.height = config.height;
|
|
209
338
|
|
|
339
|
+
DragAcceptFiles(w.pImpl->hwnd, config.enableFileDrop ? TRUE : FALSE);
|
|
340
|
+
|
|
210
341
|
if (config.center) {
|
|
211
342
|
RECT screen;
|
|
212
343
|
SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
|
|
@@ -801,6 +932,8 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
801
932
|
controller->get_ParentWindow(&parentHwnd);
|
|
802
933
|
GetClientRect(parentHwnd, &bounds);
|
|
803
934
|
controller->put_Bounds(bounds);
|
|
935
|
+
Window::Impl::embeddedWebviewByParent[parentHwnd] =
|
|
936
|
+
pImpl.get();
|
|
804
937
|
|
|
805
938
|
pImpl->nativeWebView = pImpl->webview.Get();
|
|
806
939
|
pImpl->ready = true;
|
|
@@ -841,6 +974,11 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
841
974
|
|
|
842
975
|
var block = function(e) {
|
|
843
976
|
if (!e) return false;
|
|
977
|
+
if (e.type === 'dragenter') {
|
|
978
|
+
window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragEnter', { detail: {} }));
|
|
979
|
+
} else if (e.type === 'dragleave') {
|
|
980
|
+
window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragLeave', { detail: {} }));
|
|
981
|
+
}
|
|
844
982
|
e.preventDefault();
|
|
845
983
|
e.stopPropagation();
|
|
846
984
|
if (typeof e.stopImmediatePropagation === 'function') {
|
|
@@ -1117,6 +1255,48 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1117
1255
|
result = canForward ? "true" : "false";
|
|
1118
1256
|
success = true;
|
|
1119
1257
|
}
|
|
1258
|
+
} else if (method.find("fileDrop.") == 0) {
|
|
1259
|
+
std::string fileDropMethod = method.substr(9);
|
|
1260
|
+
if (fileDropMethod == "setEnabled") {
|
|
1261
|
+
size_t p1 = msg.find("[");
|
|
1262
|
+
size_t p2 = msg.find("]");
|
|
1263
|
+
if (p1 != std::string::npos &&
|
|
1264
|
+
p2 != std::string::npos) {
|
|
1265
|
+
std::string params =
|
|
1266
|
+
msg.substr(p1 + 1, p2 - p1 - 1);
|
|
1267
|
+
bool enabled =
|
|
1268
|
+
params.find("true") !=
|
|
1269
|
+
std::string::npos;
|
|
1270
|
+
pImpl->config.enableFileDrop = enabled;
|
|
1271
|
+
|
|
1272
|
+
HWND targetHwnd = nullptr;
|
|
1273
|
+
if (pImpl->window) {
|
|
1274
|
+
targetHwnd = static_cast<HWND>(
|
|
1275
|
+
pImpl->window->nativeHandle());
|
|
1276
|
+
}
|
|
1277
|
+
if (!targetHwnd && pImpl->controller) {
|
|
1278
|
+
pImpl->controller->get_ParentWindow(
|
|
1279
|
+
&targetHwnd);
|
|
1280
|
+
}
|
|
1281
|
+
if (targetHwnd) {
|
|
1282
|
+
DragAcceptFiles(targetHwnd,
|
|
1283
|
+
enabled ? TRUE
|
|
1284
|
+
: FALSE);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
success = true;
|
|
1288
|
+
} else if (fileDropMethod == "isEnabled") {
|
|
1289
|
+
result = pImpl->config.enableFileDrop
|
|
1290
|
+
? "true"
|
|
1291
|
+
: "false";
|
|
1292
|
+
success = true;
|
|
1293
|
+
} else if (fileDropMethod == "startDrag") {
|
|
1294
|
+
result = "false";
|
|
1295
|
+
success = true;
|
|
1296
|
+
} else if (fileDropMethod ==
|
|
1297
|
+
"clearCallbacks") {
|
|
1298
|
+
success = true;
|
|
1299
|
+
}
|
|
1120
1300
|
}
|
|
1121
1301
|
|
|
1122
1302
|
// Send response back to JS (matches
|
|
@@ -1192,6 +1372,8 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1192
1372
|
"window.__plusui_nativeFileDropOnly = true;"
|
|
1193
1373
|
"var block = function(e) {"
|
|
1194
1374
|
"if (!e) return false;"
|
|
1375
|
+
"if (e.type === 'dragenter') { window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragEnter', { detail: {} })); }"
|
|
1376
|
+
"else if (e.type === 'dragleave') { window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragLeave', { detail: {} })); }"
|
|
1195
1377
|
"e.preventDefault();"
|
|
1196
1378
|
"e.stopPropagation();"
|
|
1197
1379
|
"if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();"
|
|
@@ -1672,6 +1854,11 @@ void Window::setWebviewDragDropEnabled(bool enabled) {
|
|
|
1672
1854
|
|
|
1673
1855
|
var block = function(e) {
|
|
1674
1856
|
if (!e) return false;
|
|
1857
|
+
if (e.type === 'dragenter') {
|
|
1858
|
+
window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragEnter', { detail: {} }));
|
|
1859
|
+
} else if (e.type === 'dragleave') {
|
|
1860
|
+
window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragLeave', { detail: {} }));
|
|
1861
|
+
}
|
|
1675
1862
|
e.preventDefault();
|
|
1676
1863
|
e.stopPropagation();
|
|
1677
1864
|
if (typeof e.stopImmediatePropagation === 'function') {
|