plusui-native-core 0.1.102 → 0.1.104
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/API/Connect_API.ts +62 -146
- package/Core/Features/API/filedrop-api.ts +12 -1
- package/Core/Features/API/index.ts +3 -4
- package/Core/Features/App/app.cpp +32 -32
- package/Core/Features/Connection/README.md +176 -52
- package/Core/Features/Connection/connect.ts +136 -21
- package/Core/Features/FileDrop/filedrop.ts +139 -36
- package/Core/Features/Keyboard/keyboard.cpp +218 -189
- package/Core/Features/Keyboard/keyboard.ts +100 -31
- package/Core/Features/Keyboard/keyboard_linux.cpp +226 -0
- package/Core/Features/Keyboard/keyboard_macos.cpp +220 -0
- package/Core/Features/Keyboard/keyboard_windows.cpp +56 -0
- package/Core/Features/Window/webview.cpp +248 -57
- package/Core/Features/Window/webview.ts +1 -1
- package/Core/Features/Window/window.cpp +433 -63
- package/Core/include/plusui/api.hpp +2 -2
- package/Core/include/plusui/app.hpp +41 -41
- package/Core/include/plusui/connect.hpp +125 -81
- package/Core/include/plusui/window.hpp +38 -40
- package/package.json +1 -1
|
@@ -17,15 +17,18 @@
|
|
|
17
17
|
#include <wrl.h>
|
|
18
18
|
using namespace Microsoft::WRL;
|
|
19
19
|
#pragma warning(pop)
|
|
20
|
-
#elif defined(__APPLE__)
|
|
21
|
-
#include <Cocoa/Cocoa.h>
|
|
22
|
-
#import <
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
#
|
|
20
|
+
#elif defined(__APPLE__)
|
|
21
|
+
#include <Cocoa/Cocoa.h>
|
|
22
|
+
#import <AppKit/AppKit.h>
|
|
23
|
+
#import <Carbon/Carbon.h>
|
|
24
|
+
#import <WebKit/WebKit.h>
|
|
25
|
+
#include <objc/objc-runtime.h>
|
|
26
|
+
|
|
27
|
+
#else
|
|
28
|
+
#include <gdk/gdk.h>
|
|
29
|
+
#include <gtk/gtk.h>
|
|
30
|
+
#include <webkit2/webkit2.h>
|
|
31
|
+
#endif
|
|
29
32
|
|
|
30
33
|
#include <stb_image.h>
|
|
31
34
|
|
|
@@ -82,6 +85,42 @@ static std::string jsonEscape(const std::string &input) {
|
|
|
82
85
|
return out;
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
// Map Windows VK code to PlusUI KeyCode (GLFW-style values)
|
|
89
|
+
static int vkToPlusUIKeyCode(WPARAM vk) {
|
|
90
|
+
if (vk >= 'A' && vk <= 'Z') return (int)vk; // A=65…Z=90
|
|
91
|
+
if (vk >= '0' && vk <= '9') return (int)vk; // 0=48…9=57
|
|
92
|
+
if (vk >= VK_F1 && vk <= VK_F12) return 290 + (int)(vk - VK_F1); // F1=290…F12=301
|
|
93
|
+
switch (vk) {
|
|
94
|
+
case VK_SPACE: return 32;
|
|
95
|
+
case VK_ESCAPE: return 256;
|
|
96
|
+
case VK_RETURN: return 257;
|
|
97
|
+
case VK_TAB: return 258;
|
|
98
|
+
case VK_BACK: return 259;
|
|
99
|
+
case VK_DELETE: return 261;
|
|
100
|
+
case VK_RIGHT: return 262;
|
|
101
|
+
case VK_LEFT: return 263;
|
|
102
|
+
case VK_DOWN: return 264;
|
|
103
|
+
case VK_UP: return 265;
|
|
104
|
+
case VK_SHIFT:
|
|
105
|
+
case VK_LSHIFT: return 340;
|
|
106
|
+
case VK_CONTROL:
|
|
107
|
+
case VK_LCONTROL:return 341;
|
|
108
|
+
case VK_MENU:
|
|
109
|
+
case VK_LMENU: return 342;
|
|
110
|
+
default: return 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Build the KeyMod flags from current modifier key states
|
|
115
|
+
static int currentKeyMods() {
|
|
116
|
+
int mods = 0;
|
|
117
|
+
if (GetKeyState(VK_SHIFT) & 0x8000) mods |= 1; // KeyMod::Shift
|
|
118
|
+
if (GetKeyState(VK_CONTROL) & 0x8000) mods |= 2; // KeyMod::Control
|
|
119
|
+
if (GetKeyState(VK_MENU) & 0x8000) mods |= 4; // KeyMod::Alt
|
|
120
|
+
if ((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) mods |= 8; // KeyMod::Super
|
|
121
|
+
return mods;
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
static std::string mimeTypeFromPath(const std::string &path) {
|
|
86
125
|
size_t dot = path.find_last_of('.');
|
|
87
126
|
if (dot == std::string::npos)
|
|
@@ -105,10 +144,43 @@ static std::string mimeTypeFromPath(const std::string &path) {
|
|
|
105
144
|
return "application/json";
|
|
106
145
|
if (ext == ".pdf")
|
|
107
146
|
return "application/pdf";
|
|
108
|
-
return "application/octet-stream";
|
|
109
|
-
}
|
|
110
|
-
#endif
|
|
111
|
-
|
|
147
|
+
return "application/octet-stream";
|
|
148
|
+
}
|
|
149
|
+
#endif // _WIN32
|
|
150
|
+
|
|
151
|
+
#if !defined(_WIN32)
|
|
152
|
+
// Build the JS snippet that dispatches plusui:keyboard:keydown/keyup.
|
|
153
|
+
// Used by macOS (NSEvent monitor) and Linux (GDK key signals).
|
|
154
|
+
static std::string buildKeyEventScript(int keyCode, int scancode, int mods,
|
|
155
|
+
bool pressed, bool repeat) {
|
|
156
|
+
std::string evtName = pressed ? "plusui:keyboard:keydown"
|
|
157
|
+
: "plusui:keyboard:keyup";
|
|
158
|
+
return
|
|
159
|
+
"(function(){"
|
|
160
|
+
"var e={key:" + std::to_string(keyCode) +
|
|
161
|
+
",scancode:" + std::to_string(scancode) +
|
|
162
|
+
",mods:" + std::to_string(mods) +
|
|
163
|
+
",pressed:" + (pressed ? "true" : "false") +
|
|
164
|
+
",repeat:" + (repeat ? "true" : "false") +
|
|
165
|
+
",keyName:\"\"};"
|
|
166
|
+
"window.dispatchEvent(new CustomEvent('" + evtName + "',{detail:e}));"
|
|
167
|
+
"})();";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Build the JS snippet that dispatches plusui:keyboard:shortcut and calls handler.
|
|
171
|
+
static std::string buildShortcutScript(const std::string& id) {
|
|
172
|
+
return
|
|
173
|
+
"(function(){"
|
|
174
|
+
"var id=\"" + id + "\";"
|
|
175
|
+
"window.dispatchEvent(new CustomEvent('plusui:keyboard:shortcut',{detail:{id:id}}));"
|
|
176
|
+
"if(window.__plusui_shortcut_handlers__&&window.__plusui_shortcut_handlers__[id]){"
|
|
177
|
+
"window.__plusui_shortcut_handlers__[id]();"
|
|
178
|
+
"}"
|
|
179
|
+
"})();";
|
|
180
|
+
}
|
|
181
|
+
#endif // !_WIN32
|
|
182
|
+
|
|
183
|
+
} // namespace
|
|
112
184
|
|
|
113
185
|
struct Window::Impl {
|
|
114
186
|
void *nativeWindow = nullptr;
|
|
@@ -137,8 +209,9 @@ struct Window::Impl {
|
|
|
137
209
|
ErrorCallback errorCallback;
|
|
138
210
|
ConsoleCallback consoleCallback;
|
|
139
211
|
MessageCallback messageCallback;
|
|
140
|
-
FileDropCallback fileDropCallback;
|
|
141
|
-
std::map<std::string, JSCallback> bindings;
|
|
212
|
+
FileDropCallback fileDropCallback;
|
|
213
|
+
std::map<std::string, JSCallback> bindings;
|
|
214
|
+
std::map<int, std::string> hotkeys; // hotkey id -> shortcut id
|
|
142
215
|
|
|
143
216
|
std::vector<MoveCallback> moveCallbacks;
|
|
144
217
|
std::vector<ResizeCallback> resizeCallbacks;
|
|
@@ -197,7 +270,7 @@ struct Window::Impl {
|
|
|
197
270
|
}
|
|
198
271
|
}
|
|
199
272
|
|
|
200
|
-
if (!targetImpl || !targetImpl->config.
|
|
273
|
+
if (!targetImpl || !targetImpl->config.fileDrop ||
|
|
201
274
|
!targetImpl->webview) {
|
|
202
275
|
DragFinish(hDrop);
|
|
203
276
|
return 0;
|
|
@@ -257,12 +330,16 @@ struct Window::Impl {
|
|
|
257
330
|
targetImpl->fileDropCallback(filesJson);
|
|
258
331
|
}
|
|
259
332
|
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
333
|
+
// DragQueryPoint returns physical (device) pixels relative to the
|
|
334
|
+
// client area. document.elementFromPoint uses CSS (logical) pixels.
|
|
335
|
+
// Divide by the DPI scale factor so the hit-test is correct on HiDPI
|
|
336
|
+
// displays (e.g. 150% scaling → divide by 1.5).
|
|
337
|
+
double dpiScale = 1.0;
|
|
338
|
+
UINT dpi = GetDpiForWindow(hwnd);
|
|
339
|
+
if (dpi > 0) dpiScale = static_cast<double>(dpi) / 96.0;
|
|
340
|
+
|
|
341
|
+
int dpx = static_cast<int>(dropPoint.x / dpiScale);
|
|
342
|
+
int dpy = static_cast<int>(dropPoint.y / dpiScale);
|
|
266
343
|
|
|
267
344
|
std::string eventScript =
|
|
268
345
|
"(function(){"
|
|
@@ -273,7 +350,7 @@ struct Window::Impl {
|
|
|
273
350
|
"window.dispatchEvent(new "
|
|
274
351
|
"CustomEvent('plusui:fileDrop.filesDropped',"
|
|
275
352
|
" { detail: { files: files } }));"
|
|
276
|
-
// Zone-specific delivery
|
|
353
|
+
// Zone-specific delivery via DPI-corrected hit test
|
|
277
354
|
"var el=document.elementFromPoint(" +
|
|
278
355
|
std::to_string(dpx) + "," + std::to_string(dpy) +
|
|
279
356
|
");"
|
|
@@ -282,10 +359,9 @@ struct Window::Impl {
|
|
|
282
359
|
"if(zoneName&&window.__plusui_fileDrop__){"
|
|
283
360
|
"window.__plusui_fileDrop__(zoneName,files);"
|
|
284
361
|
"}"
|
|
285
|
-
// If no zone matched
|
|
286
|
-
//
|
|
287
|
-
"if(!zoneName&&window.
|
|
288
|
-
"window.__plusui_fileDrop_default__){"
|
|
362
|
+
// If no zone matched deliver to all registered zones so a
|
|
363
|
+
// single-zone app always works regardless of hit-test accuracy.
|
|
364
|
+
"if(!zoneName&&window.__plusui_fileDrop_default__){"
|
|
289
365
|
"window.__plusui_fileDrop_default__(files);"
|
|
290
366
|
"}"
|
|
291
367
|
"})();";
|
|
@@ -295,20 +371,70 @@ struct Window::Impl {
|
|
|
295
371
|
nullptr);
|
|
296
372
|
return 0;
|
|
297
373
|
}
|
|
298
|
-
case
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
374
|
+
case WM_HOTKEY: {
|
|
375
|
+
int hotKeyId = (int)wp;
|
|
376
|
+
auto it = impl->hotkeys.find(hotKeyId);
|
|
377
|
+
if (it != impl->hotkeys.end() && impl->webview) {
|
|
378
|
+
std::string shortcutId = it->second;
|
|
379
|
+
// Fire keyboard:shortcut event and also trigger any registered
|
|
380
|
+
// shortcutHandlers on the JS side
|
|
381
|
+
std::string script =
|
|
382
|
+
"(function(){"
|
|
383
|
+
"var id=" + std::string("\"") + shortcutId + "\";"
|
|
384
|
+
"window.dispatchEvent(new CustomEvent('plusui:keyboard:shortcut',{detail:{id:id}}));"
|
|
385
|
+
"if(window.__plusui_shortcut_handlers__&&window.__plusui_shortcut_handlers__[id]){"
|
|
386
|
+
"window.__plusui_shortcut_handlers__[id]();"
|
|
387
|
+
"}"
|
|
388
|
+
"})();";
|
|
389
|
+
impl->webview->ExecuteScript(
|
|
390
|
+
std::wstring(script.begin(), script.end()).c_str(), nullptr);
|
|
391
|
+
}
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
case WM_KEYDOWN:
|
|
395
|
+
case WM_SYSKEYDOWN:
|
|
396
|
+
case WM_KEYUP:
|
|
397
|
+
case WM_SYSKEYUP: {
|
|
398
|
+
if (impl->webview) {
|
|
399
|
+
bool pressed = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
|
|
400
|
+
bool repeat = pressed && ((lp & 0x40000000) != 0);
|
|
401
|
+
int keyCode = vkToPlusUIKeyCode(wp);
|
|
402
|
+
if (keyCode != 0) {
|
|
403
|
+
int mods = currentKeyMods();
|
|
404
|
+
// Build a KeyEvent JSON object and dispatch as CustomEvent
|
|
405
|
+
std::string evtName = pressed
|
|
406
|
+
? "plusui:keyboard:keydown"
|
|
407
|
+
: "plusui:keyboard:keyup";
|
|
408
|
+
std::string script =
|
|
409
|
+
"(function(){"
|
|
410
|
+
"var e={key:" + std::to_string(keyCode) +
|
|
411
|
+
",scancode:" + std::to_string((int)((lp >> 16) & 0xFF)) +
|
|
412
|
+
",mods:" + std::to_string(mods) +
|
|
413
|
+
",pressed:" + (pressed ? "true" : "false") +
|
|
414
|
+
",repeat:" + (repeat ? "true" : "false") +
|
|
415
|
+
",keyName:\"\"};"
|
|
416
|
+
"window.dispatchEvent(new CustomEvent('" + evtName + "',{detail:e}));"
|
|
417
|
+
"})();";
|
|
418
|
+
impl->webview->ExecuteScript(
|
|
419
|
+
std::wstring(script.begin(), script.end()).c_str(), nullptr);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case WM_SETFOCUS:
|
|
425
|
+
impl->state.isFocused = true;
|
|
426
|
+
for (auto &cb : impl->focusCallbacks)
|
|
427
|
+
cb(true);
|
|
428
|
+
break;
|
|
429
|
+
case WM_KILLFOCUS:
|
|
430
|
+
impl->state.isFocused = false;
|
|
431
|
+
for (auto &cb : impl->focusCallbacks)
|
|
432
|
+
cb(false);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return DefWindowProc(hwnd, msg, wp, lp);
|
|
437
|
+
}
|
|
312
438
|
#elif defined(__APPLE__)
|
|
313
439
|
WKWebView *wkWebView = nullptr;
|
|
314
440
|
#else
|
|
@@ -378,7 +504,7 @@ Window Window::create(const WindowConfig &config) {
|
|
|
378
504
|
w.pImpl->state.width = config.width;
|
|
379
505
|
w.pImpl->state.height = config.height;
|
|
380
506
|
|
|
381
|
-
DragAcceptFiles(w.pImpl->hwnd, config.
|
|
507
|
+
DragAcceptFiles(w.pImpl->hwnd, config.fileDrop ? TRUE : FALSE);
|
|
382
508
|
|
|
383
509
|
if (config.center) {
|
|
384
510
|
RECT screen;
|
|
@@ -943,7 +1069,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
943
1069
|
|
|
944
1070
|
// Pure native file-drop mode: when native FileDrop is enabled,
|
|
945
1071
|
// fully disable browser/WebView drag-drop handling.
|
|
946
|
-
if (win.pImpl->config.
|
|
1072
|
+
if (win.pImpl->config.fileDrop) {
|
|
947
1073
|
win.pImpl->config.disableWebviewDragDrop = true;
|
|
948
1074
|
}
|
|
949
1075
|
|
|
@@ -978,16 +1104,18 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
978
1104
|
Window::Impl::embeddedWebviewByParent[parentHwnd] =
|
|
979
1105
|
pImpl.get();
|
|
980
1106
|
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
1107
|
+
// Disable WebView2's internal drop handling so the OS
|
|
1108
|
+
// drop message (WM_DROPFILES) propagates to our WndProc.
|
|
1109
|
+
// When AllowExternalDrop is TRUE, WebView2 consumes the
|
|
1110
|
+
// drop event itself and WM_DROPFILES never fires.
|
|
1111
|
+
// Visual drag feedback (dropzone-active CSS class) is
|
|
1112
|
+
// still handled by the injected JS via dragenter/dragover.
|
|
985
1113
|
ComPtr<ICoreWebView2Controller4> controller4;
|
|
986
1114
|
if (controller &&
|
|
987
1115
|
SUCCEEDED(controller->QueryInterface(
|
|
988
1116
|
IID_PPV_ARGS(&controller4))) &&
|
|
989
1117
|
controller4) {
|
|
990
|
-
controller4->put_AllowExternalDrop(
|
|
1118
|
+
controller4->put_AllowExternalDrop(FALSE);
|
|
991
1119
|
}
|
|
992
1120
|
|
|
993
1121
|
pImpl->nativeWebView = pImpl->webview.Get();
|
|
@@ -1054,7 +1182,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1054
1182
|
var zone = findDropZone(e);
|
|
1055
1183
|
updateActiveZone(zone);
|
|
1056
1184
|
if (e.dataTransfer) {
|
|
1057
|
-
try { e.dataTransfer.dropEffect = zone ? '
|
|
1185
|
+
try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
|
|
1058
1186
|
}
|
|
1059
1187
|
}, true);
|
|
1060
1188
|
|
|
@@ -1063,7 +1191,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1063
1191
|
var zone = findDropZone(e);
|
|
1064
1192
|
updateActiveZone(zone);
|
|
1065
1193
|
if (e.dataTransfer) {
|
|
1066
|
-
try { e.dataTransfer.dropEffect = zone ? '
|
|
1194
|
+
try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
|
|
1067
1195
|
}
|
|
1068
1196
|
}, true);
|
|
1069
1197
|
|
|
@@ -1491,7 +1619,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1491
1619
|
msg.substr(p1 + 1, p2 - p1 - 1);
|
|
1492
1620
|
bool enabled = params.find("true") !=
|
|
1493
1621
|
std::string::npos;
|
|
1494
|
-
pImpl->config.
|
|
1622
|
+
pImpl->config.fileDrop = enabled;
|
|
1495
1623
|
|
|
1496
1624
|
HWND targetHwnd = nullptr;
|
|
1497
1625
|
if (pImpl->window) {
|
|
@@ -1524,7 +1652,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1524
1652
|
}
|
|
1525
1653
|
success = true;
|
|
1526
1654
|
} else if (fileDropMethod == "isEnabled") {
|
|
1527
|
-
result = pImpl->config.
|
|
1655
|
+
result = pImpl->config.fileDrop
|
|
1528
1656
|
? "true"
|
|
1529
1657
|
: "false";
|
|
1530
1658
|
success = true;
|
|
@@ -1630,7 +1758,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1630
1758
|
// Block browser default drag-drop while allowing drop zone visual feedback.
|
|
1631
1759
|
// File delivery is handled natively by macOS drag APIs, not browser events.
|
|
1632
1760
|
if (win.pImpl->config.disableWebviewDragDrop ||
|
|
1633
|
-
win.pImpl->config.
|
|
1761
|
+
win.pImpl->config.fileDrop) {
|
|
1634
1762
|
NSString *disableDragDropScript =
|
|
1635
1763
|
@"(function() {"
|
|
1636
1764
|
"if (window.__plusui_dropzone_init) return;"
|
|
@@ -1666,7 +1794,9 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1666
1794
|
"document.addEventListener('dragleave', function(e) {"
|
|
1667
1795
|
"e.preventDefault();"
|
|
1668
1796
|
"var zone = findDropZone(e);"
|
|
1669
|
-
"
|
|
1797
|
+
"if (e.relatedTarget && zone && !zone.contains(e.relatedTarget)) {"
|
|
1798
|
+
"updateActiveZone(null);"
|
|
1799
|
+
"}"
|
|
1670
1800
|
"}, true);"
|
|
1671
1801
|
"document.addEventListener('drop', function(e) {"
|
|
1672
1802
|
"e.preventDefault();"
|
|
@@ -1700,15 +1830,255 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1700
1830
|
[[WKWebView alloc] initWithFrame:parentView.bounds configuration:config];
|
|
1701
1831
|
[parentView addSubview:win.pImpl->wkWebView];
|
|
1702
1832
|
|
|
1703
|
-
win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1833
|
+
win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
|
|
1834
|
+
|
|
1835
|
+
// ── macOS key event forwarding ─────────────────────────────────────────────
|
|
1836
|
+
// NSEvent local monitor: fires for every keyDown/keyUp that goes through
|
|
1837
|
+
// the app's event queue while any app window is key. We convert to
|
|
1838
|
+
// PlusUI KeyCode and dispatch as CustomEvents into the WebView.
|
|
1839
|
+
{
|
|
1840
|
+
WKWebView * __weak wv = win.pImpl->wkWebView;
|
|
1841
|
+
|
|
1842
|
+
// Map macOS virtual keycode + character to PlusUI KeyCode (GLFW style)
|
|
1843
|
+
auto macVKToPlusUI = [](unsigned short vk, unsigned short ch) -> int {
|
|
1844
|
+
if (ch >= 'a' && ch <= 'z') return (int)(ch - 'a' + 65); // A-Z
|
|
1845
|
+
if (ch >= 'A' && ch <= 'Z') return (int)ch;
|
|
1846
|
+
if (ch >= '0' && ch <= '9') return (int)ch; // 0-9
|
|
1847
|
+
static const struct { unsigned short vk; int code; } fkeys[] = {
|
|
1848
|
+
{122,290},{120,291},{99,292},{118,293},{96,294},{97,295},
|
|
1849
|
+
{98,296},{100,297},{101,298},{109,299},{103,300},{111,301}
|
|
1850
|
+
};
|
|
1851
|
+
for (auto& f : fkeys) if (vk == f.vk) return f.code;
|
|
1852
|
+
switch (vk) {
|
|
1853
|
+
case 49: return 32; // Space
|
|
1854
|
+
case 53: return 256; // Escape
|
|
1855
|
+
case 36: return 257; // Return
|
|
1856
|
+
case 48: return 258; // Tab
|
|
1857
|
+
case 51: return 259; // Backspace
|
|
1858
|
+
case 117: return 261; // Forward Delete
|
|
1859
|
+
case 124: return 262; // Right
|
|
1860
|
+
case 123: return 263; // Left
|
|
1861
|
+
case 125: return 264; // Down
|
|
1862
|
+
case 126: return 265; // Up
|
|
1863
|
+
case 56: return 340; // Left Shift
|
|
1864
|
+
case 59: return 341; // Left Control
|
|
1865
|
+
case 58: return 342; // Left Option/Alt
|
|
1866
|
+
default: return 0;
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
auto buildMods = [](NSEventModifierFlags f) -> int {
|
|
1871
|
+
int m = 0;
|
|
1872
|
+
if (f & NSEventModifierFlagShift) m |= 1;
|
|
1873
|
+
if (f & NSEventModifierFlagControl) m |= 2;
|
|
1874
|
+
if (f & NSEventModifierFlagOption) m |= 4;
|
|
1875
|
+
if (f & NSEventModifierFlagCommand) m |= 8;
|
|
1876
|
+
return m;
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
[NSEvent addLocalMonitorForEventsMatchingMask:
|
|
1880
|
+
NSEventMaskKeyDown | NSEventMaskKeyUp
|
|
1881
|
+
handler:^NSEvent*(NSEvent* event) {
|
|
1882
|
+
if (!wv) return event;
|
|
1883
|
+
unsigned short vk = [event keyCode];
|
|
1884
|
+
unsigned short ch = 0;
|
|
1885
|
+
NSString *chars = [event charactersIgnoringModifiers];
|
|
1886
|
+
if (chars.length > 0) ch = (unsigned short)[chars characterAtIndex:0];
|
|
1887
|
+
int keyCode = macVKToPlusUI(vk, ch);
|
|
1888
|
+
if (keyCode != 0) {
|
|
1889
|
+
bool pressed = ([event type] == NSEventTypeKeyDown);
|
|
1890
|
+
bool repeat = pressed && [event isARepeat];
|
|
1891
|
+
int mods = buildMods([event modifierFlags]);
|
|
1892
|
+
std::string script = buildKeyEventScript(keyCode, (int)vk, mods, pressed, repeat);
|
|
1893
|
+
NSString* js = [NSString stringWithUTF8String:script.c_str()];
|
|
1894
|
+
[wv evaluateJavaScript:js completionHandler:nil];
|
|
1895
|
+
}
|
|
1896
|
+
return event;
|
|
1897
|
+
}];
|
|
1898
|
+
|
|
1899
|
+
// Wire Carbon global hotkey fired callback → evaluateJavaScript.
|
|
1900
|
+
// setShortcutFiredCallback is defined in keyboard_macos.cpp (compiled via
|
|
1901
|
+
// #include in keyboard.cpp) and lives in the same plusui namespace.
|
|
1902
|
+
void setShortcutFiredCallback(std::function<void(const std::string&)>);
|
|
1903
|
+
setShortcutFiredCallback([wv](const std::string& id) {
|
|
1904
|
+
if (!wv) return;
|
|
1905
|
+
std::string script = buildShortcutScript(id);
|
|
1906
|
+
NSString* js = [NSString stringWithUTF8String:script.c_str()];
|
|
1907
|
+
[wv evaluateJavaScript:js completionHandler:nil];
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
#else
|
|
1912
|
+
// ── Linux / GTK + WebKit2 WebView ─────────────────────────────────────────
|
|
1913
|
+
{
|
|
1914
|
+
GtkWidget *gtkParent = static_cast<GtkWidget*>(windowHandle);
|
|
1915
|
+
WebKitWebView *webView =
|
|
1916
|
+
WEBKIT_WEB_VIEW(webkit_web_view_new());
|
|
1917
|
+
gtk_container_add(GTK_CONTAINER(gtkParent), GTK_WIDGET(webView));
|
|
1918
|
+
gtk_widget_show(GTK_WIDGET(webView));
|
|
1919
|
+
win.pImpl->gtkWebView = webView;
|
|
1920
|
+
win.pImpl->nativeWebView = static_cast<void*>(webView);
|
|
1921
|
+
|
|
1922
|
+
// Inject the native bridge script on every page load
|
|
1923
|
+
WebKitUserContentManager *mgr =
|
|
1924
|
+
webkit_web_view_get_user_content_manager(webView);
|
|
1925
|
+
WebKitUserScript *bridgeScript = webkit_user_script_new(
|
|
1926
|
+
"window.__native_invoke__ = function(req) {"
|
|
1927
|
+
" window.webkit.messageHandlers.plusui.postMessage(req);"
|
|
1928
|
+
"};",
|
|
1929
|
+
WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
|
|
1930
|
+
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
|
|
1931
|
+
nullptr, nullptr);
|
|
1932
|
+
webkit_user_content_manager_add_script(mgr, bridgeScript);
|
|
1933
|
+
webkit_user_script_unref(bridgeScript);
|
|
1934
|
+
|
|
1935
|
+
// Helper to run JS in this WebView
|
|
1936
|
+
auto runJS = [webView](const std::string& script) {
|
|
1937
|
+
webkit_web_view_run_javascript(webView, script.c_str(),
|
|
1938
|
+
nullptr, nullptr, nullptr);
|
|
1939
|
+
};
|
|
1940
|
+
|
|
1941
|
+
// GDK keyval → PlusUI KeyCode
|
|
1942
|
+
auto gdkKeyToPlusUI = [](guint keyval) -> int {
|
|
1943
|
+
if (keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
|
|
1944
|
+
return (int)(keyval - GDK_KEY_a + 65);
|
|
1945
|
+
if (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z)
|
|
1946
|
+
return (int)(keyval - GDK_KEY_A + 65);
|
|
1947
|
+
if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
|
|
1948
|
+
return (int)(keyval - GDK_KEY_0 + 48);
|
|
1949
|
+
if (keyval >= GDK_KEY_F1 && keyval <= GDK_KEY_F12)
|
|
1950
|
+
return 290 + (int)(keyval - GDK_KEY_F1);
|
|
1951
|
+
switch (keyval) {
|
|
1952
|
+
case GDK_KEY_space: return 32;
|
|
1953
|
+
case GDK_KEY_Escape: return 256;
|
|
1954
|
+
case GDK_KEY_Return: return 257;
|
|
1955
|
+
case GDK_KEY_Tab: return 258;
|
|
1956
|
+
case GDK_KEY_BackSpace: return 259;
|
|
1957
|
+
case GDK_KEY_Delete: return 261;
|
|
1958
|
+
case GDK_KEY_Right: return 262;
|
|
1959
|
+
case GDK_KEY_Left: return 263;
|
|
1960
|
+
case GDK_KEY_Down: return 264;
|
|
1961
|
+
case GDK_KEY_Up: return 265;
|
|
1962
|
+
case GDK_KEY_Shift_L:
|
|
1963
|
+
case GDK_KEY_Shift_R: return 340;
|
|
1964
|
+
case GDK_KEY_Control_L:
|
|
1965
|
+
case GDK_KEY_Control_R: return 341;
|
|
1966
|
+
case GDK_KEY_Alt_L:
|
|
1967
|
+
case GDK_KEY_Alt_R: return 342;
|
|
1968
|
+
default: return 0;
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
auto buildGdkMods = [](GdkModifierType state) -> int {
|
|
1973
|
+
int m = 0;
|
|
1974
|
+
if (state & GDK_SHIFT_MASK) m |= 1;
|
|
1975
|
+
if (state & GDK_CONTROL_MASK) m |= 2;
|
|
1976
|
+
if (state & GDK_MOD1_MASK) m |= 4; // Alt
|
|
1977
|
+
if (state & GDK_SUPER_MASK) m |= 8;
|
|
1978
|
+
return m;
|
|
1979
|
+
};
|
|
1980
|
+
|
|
1981
|
+
// Key-press callback: capture by value via malloc'd context struct
|
|
1982
|
+
struct KeyCtx {
|
|
1983
|
+
WebKitWebView* webView;
|
|
1984
|
+
std::function<int(guint, int, int, bool, bool)> dispatch;
|
|
1985
|
+
};
|
|
1986
|
+
auto* ctx = new KeyCtx();
|
|
1987
|
+
ctx->webView = webView;
|
|
1988
|
+
|
|
1989
|
+
// key-press-event
|
|
1990
|
+
g_signal_connect(gtkParent, "key-press-event",
|
|
1991
|
+
G_CALLBACK(+[](GtkWidget*, GdkEventKey* ev, gpointer data) -> gboolean {
|
|
1992
|
+
auto* c = static_cast<KeyCtx*>(data);
|
|
1993
|
+
// gdkKeyToPlusUI re-implemented inline (lambda not capturable in C cb)
|
|
1994
|
+
guint kv = ev->keyval;
|
|
1995
|
+
int kc = 0;
|
|
1996
|
+
if (kv >= GDK_KEY_a && kv <= GDK_KEY_z) kc = (int)(kv-GDK_KEY_a+65);
|
|
1997
|
+
else if (kv >= GDK_KEY_A && kv <= GDK_KEY_Z) kc = (int)(kv-GDK_KEY_A+65);
|
|
1998
|
+
else if (kv >= GDK_KEY_0 && kv <= GDK_KEY_9) kc = (int)(kv-GDK_KEY_0+48);
|
|
1999
|
+
else if (kv >= GDK_KEY_F1 && kv <= GDK_KEY_F12) kc = 290+(int)(kv-GDK_KEY_F1);
|
|
2000
|
+
else switch (kv) {
|
|
2001
|
+
case GDK_KEY_space: kc=32; break;
|
|
2002
|
+
case GDK_KEY_Escape: kc=256; break;
|
|
2003
|
+
case GDK_KEY_Return: kc=257; break;
|
|
2004
|
+
case GDK_KEY_Tab: kc=258; break;
|
|
2005
|
+
case GDK_KEY_BackSpace: kc=259; break;
|
|
2006
|
+
case GDK_KEY_Delete: kc=261; break;
|
|
2007
|
+
case GDK_KEY_Right: kc=262; break;
|
|
2008
|
+
case GDK_KEY_Left: kc=263; break;
|
|
2009
|
+
case GDK_KEY_Down: kc=264; break;
|
|
2010
|
+
case GDK_KEY_Up: kc=265; break;
|
|
2011
|
+
case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: kc=340; break;
|
|
2012
|
+
case GDK_KEY_Control_L: case GDK_KEY_Control_R: kc=341; break;
|
|
2013
|
+
case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: kc=342; break;
|
|
2014
|
+
}
|
|
2015
|
+
if (!kc) return FALSE;
|
|
2016
|
+
int mods = 0;
|
|
2017
|
+
if (ev->state & GDK_SHIFT_MASK) mods |= 1;
|
|
2018
|
+
if (ev->state & GDK_CONTROL_MASK) mods |= 2;
|
|
2019
|
+
if (ev->state & GDK_MOD1_MASK) mods |= 4;
|
|
2020
|
+
if (ev->state & GDK_SUPER_MASK) mods |= 8;
|
|
2021
|
+
bool repeat = (ev->send_event == FALSE &&
|
|
2022
|
+
(ev->state & GDK_KEY_PRESS) != 0); // best-effort
|
|
2023
|
+
std::string script = buildKeyEventScript(kc, (int)ev->hardware_keycode, mods, true, repeat);
|
|
2024
|
+
webkit_web_view_run_javascript(c->webView, script.c_str(), nullptr, nullptr, nullptr);
|
|
2025
|
+
return FALSE; // don't consume — let WebKit still receive it
|
|
2026
|
+
}),
|
|
2027
|
+
ctx);
|
|
2028
|
+
|
|
2029
|
+
// key-release-event
|
|
2030
|
+
g_signal_connect(gtkParent, "key-release-event",
|
|
2031
|
+
G_CALLBACK(+[](GtkWidget*, GdkEventKey* ev, gpointer data) -> gboolean {
|
|
2032
|
+
auto* c = static_cast<KeyCtx*>(data);
|
|
2033
|
+
guint kv = ev->keyval;
|
|
2034
|
+
int kc = 0;
|
|
2035
|
+
if (kv >= GDK_KEY_a && kv <= GDK_KEY_z) kc = (int)(kv-GDK_KEY_a+65);
|
|
2036
|
+
else if (kv >= GDK_KEY_A && kv <= GDK_KEY_Z) kc = (int)(kv-GDK_KEY_A+65);
|
|
2037
|
+
else if (kv >= GDK_KEY_0 && kv <= GDK_KEY_9) kc = (int)(kv-GDK_KEY_0+48);
|
|
2038
|
+
else if (kv >= GDK_KEY_F1 && kv <= GDK_KEY_F12) kc = 290+(int)(kv-GDK_KEY_F1);
|
|
2039
|
+
else switch (kv) {
|
|
2040
|
+
case GDK_KEY_space: kc=32; break;
|
|
2041
|
+
case GDK_KEY_Escape: kc=256; break;
|
|
2042
|
+
case GDK_KEY_Return: kc=257; break;
|
|
2043
|
+
case GDK_KEY_Tab: kc=258; break;
|
|
2044
|
+
case GDK_KEY_BackSpace: kc=259; break;
|
|
2045
|
+
case GDK_KEY_Delete: kc=261; break;
|
|
2046
|
+
case GDK_KEY_Right: kc=262; break;
|
|
2047
|
+
case GDK_KEY_Left: kc=263; break;
|
|
2048
|
+
case GDK_KEY_Down: kc=264; break;
|
|
2049
|
+
case GDK_KEY_Up: kc=265; break;
|
|
2050
|
+
case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: kc=340; break;
|
|
2051
|
+
case GDK_KEY_Control_L: case GDK_KEY_Control_R: kc=341; break;
|
|
2052
|
+
case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: kc=342; break;
|
|
2053
|
+
}
|
|
2054
|
+
if (!kc) return FALSE;
|
|
2055
|
+
int mods = 0;
|
|
2056
|
+
if (ev->state & GDK_SHIFT_MASK) mods |= 1;
|
|
2057
|
+
if (ev->state & GDK_CONTROL_MASK) mods |= 2;
|
|
2058
|
+
if (ev->state & GDK_MOD1_MASK) mods |= 4;
|
|
2059
|
+
if (ev->state & GDK_SUPER_MASK) mods |= 8;
|
|
2060
|
+
std::string script = buildKeyEventScript(kc, (int)ev->hardware_keycode, mods, false, false);
|
|
2061
|
+
webkit_web_view_run_javascript(c->webView, script.c_str(), nullptr, nullptr, nullptr);
|
|
2062
|
+
return FALSE;
|
|
2063
|
+
}),
|
|
2064
|
+
ctx);
|
|
2065
|
+
|
|
2066
|
+
// Wire XGrabKey global shortcut fired callback → webkit_web_view_run_javascript.
|
|
2067
|
+
// g_shortcutFiredFn is defined in keyboard_linux.cpp (compiled via #include).
|
|
2068
|
+
extern std::function<void(const std::string&)> g_shortcutFiredFn;
|
|
2069
|
+
g_shortcutFiredFn = [webView](const std::string& id) {
|
|
2070
|
+
std::string script = buildShortcutScript(id);
|
|
2071
|
+
webkit_web_view_run_javascript(webView, script.c_str(), nullptr, nullptr, nullptr);
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
#endif
|
|
2076
|
+
|
|
2077
|
+
win.pImpl->trayManager = std::make_unique<TrayManager>();
|
|
2078
|
+
win.pImpl->trayManager->setWindowHandle(windowHandle);
|
|
2079
|
+
|
|
2080
|
+
return win;
|
|
2081
|
+
}
|
|
1712
2082
|
|
|
1713
2083
|
TrayManager &Window::tray() { return *pImpl->trayManager; }
|
|
1714
2084
|
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
* .width(1200) .width(1200)
|
|
75
75
|
* .height(800) .height(800)
|
|
76
76
|
* .devtools(true) .devtools(true)
|
|
77
|
-
* .
|
|
77
|
+
* .fileDrop(true); .fileDrop(true)
|
|
78
78
|
* .build() ← returns Window
|
|
79
79
|
* TS: app.quit() C++: app.quit()
|
|
80
80
|
* TS: app.onReady(cb) C++: (start after build())
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
* .title("My App")
|
|
202
202
|
* .width(1200)
|
|
203
203
|
* .height(800)
|
|
204
|
-
* .
|
|
204
|
+
* .fileDrop(true)
|
|
205
205
|
* .build();
|
|
206
206
|
*
|
|
207
207
|
* MyApp connect;
|