plusui-native-core 0.1.101 → 0.1.103

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.
@@ -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 <WebKit/WebKit.h>
23
- #include <objc/objc-runtime.h>
24
-
25
- #else
26
- #include <gtk/gtk.h>
27
- #include <webkit2/webkit2.h>
28
- #endif
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
- } // namespace
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;
@@ -257,12 +330,16 @@ struct Window::Impl {
257
330
  targetImpl->fileDropCallback(filesJson);
258
331
  }
259
332
 
260
- // Build the event script that dispatches to the frontend.
261
- // We always fire the global CustomEvent so any listener can react.
262
- // If the drop lands on a [data-dropzone] element we also call the
263
- // zone-specific callback (__plusui_fileDrop__).
264
- int dpx = dropPoint.x;
265
- int dpy = dropPoint.y;
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 but there is only one registered zone,
286
- // deliver there anyway so a simple single-zone app always works.
287
- "if(!zoneName&&window.__plusui_fileDrop__&&"
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 WM_SETFOCUS:
299
- impl->state.isFocused = true;
300
- for (auto &cb : impl->focusCallbacks)
301
- cb(true);
302
- break;
303
- case WM_KILLFOCUS:
304
- impl->state.isFocused = false;
305
- for (auto &cb : impl->focusCallbacks)
306
- cb(false);
307
- break;
308
- }
309
- }
310
- return DefWindowProc(hwnd, msg, wp, lp);
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
@@ -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
- // Always allow external drops at the WebView2 level
982
- // so that WM_DROPFILES on the parent HWND fires.
983
- // Browser-level drop behavior (navigating to the file)
984
- // is blocked by the injected JS below instead.
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(TRUE);
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 ? 'copy' : 'none'; } catch(_) {}
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 ? 'copy' : 'none'; } catch(_) {}
1194
+ try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
1067
1195
  }
1068
1196
  }, true);
1069
1197
 
@@ -1700,15 +1828,255 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1700
1828
  [[WKWebView alloc] initWithFrame:parentView.bounds configuration:config];
1701
1829
  [parentView addSubview:win.pImpl->wkWebView];
1702
1830
 
1703
- win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
1704
-
1705
- #endif
1706
-
1707
- win.pImpl->trayManager = std::make_unique<TrayManager>();
1708
- win.pImpl->trayManager->setWindowHandle(windowHandle);
1709
-
1710
- return win;
1711
- }
1831
+ win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
1832
+
1833
+ // ── macOS key event forwarding ─────────────────────────────────────────────
1834
+ // NSEvent local monitor: fires for every keyDown/keyUp that goes through
1835
+ // the app's event queue while any app window is key. We convert to
1836
+ // PlusUI KeyCode and dispatch as CustomEvents into the WebView.
1837
+ {
1838
+ WKWebView * __weak wv = win.pImpl->wkWebView;
1839
+
1840
+ // Map macOS virtual keycode + character to PlusUI KeyCode (GLFW style)
1841
+ auto macVKToPlusUI = [](unsigned short vk, unsigned short ch) -> int {
1842
+ if (ch >= 'a' && ch <= 'z') return (int)(ch - 'a' + 65); // A-Z
1843
+ if (ch >= 'A' && ch <= 'Z') return (int)ch;
1844
+ if (ch >= '0' && ch <= '9') return (int)ch; // 0-9
1845
+ static const struct { unsigned short vk; int code; } fkeys[] = {
1846
+ {122,290},{120,291},{99,292},{118,293},{96,294},{97,295},
1847
+ {98,296},{100,297},{101,298},{109,299},{103,300},{111,301}
1848
+ };
1849
+ for (auto& f : fkeys) if (vk == f.vk) return f.code;
1850
+ switch (vk) {
1851
+ case 49: return 32; // Space
1852
+ case 53: return 256; // Escape
1853
+ case 36: return 257; // Return
1854
+ case 48: return 258; // Tab
1855
+ case 51: return 259; // Backspace
1856
+ case 117: return 261; // Forward Delete
1857
+ case 124: return 262; // Right
1858
+ case 123: return 263; // Left
1859
+ case 125: return 264; // Down
1860
+ case 126: return 265; // Up
1861
+ case 56: return 340; // Left Shift
1862
+ case 59: return 341; // Left Control
1863
+ case 58: return 342; // Left Option/Alt
1864
+ default: return 0;
1865
+ }
1866
+ };
1867
+
1868
+ auto buildMods = [](NSEventModifierFlags f) -> int {
1869
+ int m = 0;
1870
+ if (f & NSEventModifierFlagShift) m |= 1;
1871
+ if (f & NSEventModifierFlagControl) m |= 2;
1872
+ if (f & NSEventModifierFlagOption) m |= 4;
1873
+ if (f & NSEventModifierFlagCommand) m |= 8;
1874
+ return m;
1875
+ };
1876
+
1877
+ [NSEvent addLocalMonitorForEventsMatchingMask:
1878
+ NSEventMaskKeyDown | NSEventMaskKeyUp
1879
+ handler:^NSEvent*(NSEvent* event) {
1880
+ if (!wv) return event;
1881
+ unsigned short vk = [event keyCode];
1882
+ unsigned short ch = 0;
1883
+ NSString *chars = [event charactersIgnoringModifiers];
1884
+ if (chars.length > 0) ch = (unsigned short)[chars characterAtIndex:0];
1885
+ int keyCode = macVKToPlusUI(vk, ch);
1886
+ if (keyCode != 0) {
1887
+ bool pressed = ([event type] == NSEventTypeKeyDown);
1888
+ bool repeat = pressed && [event isARepeat];
1889
+ int mods = buildMods([event modifierFlags]);
1890
+ std::string script = buildKeyEventScript(keyCode, (int)vk, mods, pressed, repeat);
1891
+ NSString* js = [NSString stringWithUTF8String:script.c_str()];
1892
+ [wv evaluateJavaScript:js completionHandler:nil];
1893
+ }
1894
+ return event;
1895
+ }];
1896
+
1897
+ // Wire Carbon global hotkey fired callback → evaluateJavaScript.
1898
+ // setShortcutFiredCallback is defined in keyboard_macos.cpp (compiled via
1899
+ // #include in keyboard.cpp) and lives in the same plusui namespace.
1900
+ void setShortcutFiredCallback(std::function<void(const std::string&)>);
1901
+ setShortcutFiredCallback([wv](const std::string& id) {
1902
+ if (!wv) return;
1903
+ std::string script = buildShortcutScript(id);
1904
+ NSString* js = [NSString stringWithUTF8String:script.c_str()];
1905
+ [wv evaluateJavaScript:js completionHandler:nil];
1906
+ });
1907
+ }
1908
+
1909
+ #else
1910
+ // ── Linux / GTK + WebKit2 WebView ─────────────────────────────────────────
1911
+ {
1912
+ GtkWidget *gtkParent = static_cast<GtkWidget*>(windowHandle);
1913
+ WebKitWebView *webView =
1914
+ WEBKIT_WEB_VIEW(webkit_web_view_new());
1915
+ gtk_container_add(GTK_CONTAINER(gtkParent), GTK_WIDGET(webView));
1916
+ gtk_widget_show(GTK_WIDGET(webView));
1917
+ win.pImpl->gtkWebView = webView;
1918
+ win.pImpl->nativeWebView = static_cast<void*>(webView);
1919
+
1920
+ // Inject the native bridge script on every page load
1921
+ WebKitUserContentManager *mgr =
1922
+ webkit_web_view_get_user_content_manager(webView);
1923
+ WebKitUserScript *bridgeScript = webkit_user_script_new(
1924
+ "window.__native_invoke__ = function(req) {"
1925
+ " window.webkit.messageHandlers.plusui.postMessage(req);"
1926
+ "};",
1927
+ WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
1928
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
1929
+ nullptr, nullptr);
1930
+ webkit_user_content_manager_add_script(mgr, bridgeScript);
1931
+ webkit_user_script_unref(bridgeScript);
1932
+
1933
+ // Helper to run JS in this WebView
1934
+ auto runJS = [webView](const std::string& script) {
1935
+ webkit_web_view_run_javascript(webView, script.c_str(),
1936
+ nullptr, nullptr, nullptr);
1937
+ };
1938
+
1939
+ // GDK keyval → PlusUI KeyCode
1940
+ auto gdkKeyToPlusUI = [](guint keyval) -> int {
1941
+ if (keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
1942
+ return (int)(keyval - GDK_KEY_a + 65);
1943
+ if (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z)
1944
+ return (int)(keyval - GDK_KEY_A + 65);
1945
+ if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
1946
+ return (int)(keyval - GDK_KEY_0 + 48);
1947
+ if (keyval >= GDK_KEY_F1 && keyval <= GDK_KEY_F12)
1948
+ return 290 + (int)(keyval - GDK_KEY_F1);
1949
+ switch (keyval) {
1950
+ case GDK_KEY_space: return 32;
1951
+ case GDK_KEY_Escape: return 256;
1952
+ case GDK_KEY_Return: return 257;
1953
+ case GDK_KEY_Tab: return 258;
1954
+ case GDK_KEY_BackSpace: return 259;
1955
+ case GDK_KEY_Delete: return 261;
1956
+ case GDK_KEY_Right: return 262;
1957
+ case GDK_KEY_Left: return 263;
1958
+ case GDK_KEY_Down: return 264;
1959
+ case GDK_KEY_Up: return 265;
1960
+ case GDK_KEY_Shift_L:
1961
+ case GDK_KEY_Shift_R: return 340;
1962
+ case GDK_KEY_Control_L:
1963
+ case GDK_KEY_Control_R: return 341;
1964
+ case GDK_KEY_Alt_L:
1965
+ case GDK_KEY_Alt_R: return 342;
1966
+ default: return 0;
1967
+ }
1968
+ };
1969
+
1970
+ auto buildGdkMods = [](GdkModifierType state) -> int {
1971
+ int m = 0;
1972
+ if (state & GDK_SHIFT_MASK) m |= 1;
1973
+ if (state & GDK_CONTROL_MASK) m |= 2;
1974
+ if (state & GDK_MOD1_MASK) m |= 4; // Alt
1975
+ if (state & GDK_SUPER_MASK) m |= 8;
1976
+ return m;
1977
+ };
1978
+
1979
+ // Key-press callback: capture by value via malloc'd context struct
1980
+ struct KeyCtx {
1981
+ WebKitWebView* webView;
1982
+ std::function<int(guint, int, int, bool, bool)> dispatch;
1983
+ };
1984
+ auto* ctx = new KeyCtx();
1985
+ ctx->webView = webView;
1986
+
1987
+ // key-press-event
1988
+ g_signal_connect(gtkParent, "key-press-event",
1989
+ G_CALLBACK(+[](GtkWidget*, GdkEventKey* ev, gpointer data) -> gboolean {
1990
+ auto* c = static_cast<KeyCtx*>(data);
1991
+ // gdkKeyToPlusUI re-implemented inline (lambda not capturable in C cb)
1992
+ guint kv = ev->keyval;
1993
+ int kc = 0;
1994
+ if (kv >= GDK_KEY_a && kv <= GDK_KEY_z) kc = (int)(kv-GDK_KEY_a+65);
1995
+ else if (kv >= GDK_KEY_A && kv <= GDK_KEY_Z) kc = (int)(kv-GDK_KEY_A+65);
1996
+ else if (kv >= GDK_KEY_0 && kv <= GDK_KEY_9) kc = (int)(kv-GDK_KEY_0+48);
1997
+ else if (kv >= GDK_KEY_F1 && kv <= GDK_KEY_F12) kc = 290+(int)(kv-GDK_KEY_F1);
1998
+ else switch (kv) {
1999
+ case GDK_KEY_space: kc=32; break;
2000
+ case GDK_KEY_Escape: kc=256; break;
2001
+ case GDK_KEY_Return: kc=257; break;
2002
+ case GDK_KEY_Tab: kc=258; break;
2003
+ case GDK_KEY_BackSpace: kc=259; break;
2004
+ case GDK_KEY_Delete: kc=261; break;
2005
+ case GDK_KEY_Right: kc=262; break;
2006
+ case GDK_KEY_Left: kc=263; break;
2007
+ case GDK_KEY_Down: kc=264; break;
2008
+ case GDK_KEY_Up: kc=265; break;
2009
+ case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: kc=340; break;
2010
+ case GDK_KEY_Control_L: case GDK_KEY_Control_R: kc=341; break;
2011
+ case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: kc=342; break;
2012
+ }
2013
+ if (!kc) return FALSE;
2014
+ int mods = 0;
2015
+ if (ev->state & GDK_SHIFT_MASK) mods |= 1;
2016
+ if (ev->state & GDK_CONTROL_MASK) mods |= 2;
2017
+ if (ev->state & GDK_MOD1_MASK) mods |= 4;
2018
+ if (ev->state & GDK_SUPER_MASK) mods |= 8;
2019
+ bool repeat = (ev->send_event == FALSE &&
2020
+ (ev->state & GDK_KEY_PRESS) != 0); // best-effort
2021
+ std::string script = buildKeyEventScript(kc, (int)ev->hardware_keycode, mods, true, repeat);
2022
+ webkit_web_view_run_javascript(c->webView, script.c_str(), nullptr, nullptr, nullptr);
2023
+ return FALSE; // don't consume — let WebKit still receive it
2024
+ }),
2025
+ ctx);
2026
+
2027
+ // key-release-event
2028
+ g_signal_connect(gtkParent, "key-release-event",
2029
+ G_CALLBACK(+[](GtkWidget*, GdkEventKey* ev, gpointer data) -> gboolean {
2030
+ auto* c = static_cast<KeyCtx*>(data);
2031
+ guint kv = ev->keyval;
2032
+ int kc = 0;
2033
+ if (kv >= GDK_KEY_a && kv <= GDK_KEY_z) kc = (int)(kv-GDK_KEY_a+65);
2034
+ else if (kv >= GDK_KEY_A && kv <= GDK_KEY_Z) kc = (int)(kv-GDK_KEY_A+65);
2035
+ else if (kv >= GDK_KEY_0 && kv <= GDK_KEY_9) kc = (int)(kv-GDK_KEY_0+48);
2036
+ else if (kv >= GDK_KEY_F1 && kv <= GDK_KEY_F12) kc = 290+(int)(kv-GDK_KEY_F1);
2037
+ else switch (kv) {
2038
+ case GDK_KEY_space: kc=32; break;
2039
+ case GDK_KEY_Escape: kc=256; break;
2040
+ case GDK_KEY_Return: kc=257; break;
2041
+ case GDK_KEY_Tab: kc=258; break;
2042
+ case GDK_KEY_BackSpace: kc=259; break;
2043
+ case GDK_KEY_Delete: kc=261; break;
2044
+ case GDK_KEY_Right: kc=262; break;
2045
+ case GDK_KEY_Left: kc=263; break;
2046
+ case GDK_KEY_Down: kc=264; break;
2047
+ case GDK_KEY_Up: kc=265; break;
2048
+ case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: kc=340; break;
2049
+ case GDK_KEY_Control_L: case GDK_KEY_Control_R: kc=341; break;
2050
+ case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: kc=342; break;
2051
+ }
2052
+ if (!kc) return FALSE;
2053
+ int mods = 0;
2054
+ if (ev->state & GDK_SHIFT_MASK) mods |= 1;
2055
+ if (ev->state & GDK_CONTROL_MASK) mods |= 2;
2056
+ if (ev->state & GDK_MOD1_MASK) mods |= 4;
2057
+ if (ev->state & GDK_SUPER_MASK) mods |= 8;
2058
+ std::string script = buildKeyEventScript(kc, (int)ev->hardware_keycode, mods, false, false);
2059
+ webkit_web_view_run_javascript(c->webView, script.c_str(), nullptr, nullptr, nullptr);
2060
+ return FALSE;
2061
+ }),
2062
+ ctx);
2063
+
2064
+ // Wire XGrabKey global shortcut fired callback → webkit_web_view_run_javascript.
2065
+ // g_shortcutFiredFn is defined in keyboard_linux.cpp (compiled via #include).
2066
+ extern std::function<void(const std::string&)> g_shortcutFiredFn;
2067
+ g_shortcutFiredFn = [webView](const std::string& id) {
2068
+ std::string script = buildShortcutScript(id);
2069
+ webkit_web_view_run_javascript(webView, script.c_str(), nullptr, nullptr, nullptr);
2070
+ };
2071
+ }
2072
+
2073
+ #endif
2074
+
2075
+ win.pImpl->trayManager = std::make_unique<TrayManager>();
2076
+ win.pImpl->trayManager->setWindowHandle(windowHandle);
2077
+
2078
+ return win;
2079
+ }
1712
2080
 
1713
2081
  TrayManager &Window::tray() { return *pImpl->trayManager; }
1714
2082