-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.ahk
More file actions
346 lines (288 loc) · 9.78 KB
/
main.ahk
File metadata and controls
346 lines (288 loc) · 9.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#Requires AutoHotkey v2.0
#SingleInstance Force
; ====== Includes (use absolute script dir to avoid include-dir issues) ======
#Include "%A_ScriptDir%\web_gui\Neutron.ahk"
; ====== Config ======
APP_TITLE := "Keyboard Dock"
; UI sizes for high DPI screens (2x scaling for 150% DPI)
; Window width = handle (48) + panel content (~240)
global UI_W_EXPANDED := 233 ; handle + 2 buttons + padding/gap
global UI_W_COLLAPSED := 64 ; handle width when collapsed (32 * 2)
global UI_H := 80 ; 45 * 2
; Gap to taskbar / screen edges
MARGIN_RIGHT := 0
MARGIN_BOTTOM := 0
; ====== State ======
global neutron := 0
global gCollapsed := false
global gKbdBlocked := false
global gDragging := false
global gDragStartX := 0
global gDragStartY := 0
; Emergency hotkey (always available): Ctrl+Alt+Backspace
^!BS::EmergencyUnblock()
; ====== Embed UI files into EXE (no extract) ======
if false {
FileInstall "html/index.html", "*"
FileInstall "html/app.css", "*"
FileInstall "html/app.js", "*"
}
; ====== Start ======
try {
InitUI()
} catch as e {
MsgBox "Startup failed: " e.Message "`n`n" e.What "`nLine: " e.Line, "Error", 16
ExitApp
}
return
; =====================================================================
; UI bootstrap
; =====================================================================
InitUI() {
global neutron, UI_W_EXPANDED, UI_H
neutron := NeutronWindow()
.Load("html/index.html")
.Opt("-Resize -MaximizeBox -MinimizeBox")
.OnEvent("Close", (*) => ExitProc())
.Show("x" GetDockX(UI_W_EXPANDED) " y" GetDockY(UI_H) " w" UI_W_EXPANDED " h" UI_H, APP_TITLE)
try {
WinSetAlwaysOnTop true, "ahk_id " neutron.hWnd
} catch {
;
}
PushStateToUI()
}
; =====================================================================
; Bridge: called from JS
; =====================================================================
Clicked(neutron, which) {
global gCollapsed, gKbdBlocked
switch which {
case "toggle":
ToggleCollapsed()
case "kbd":
ToggleKeyboardBlock()
case "exit":
ExitProc()
default:
; Unknown events are ignored for stability
return
}
}
; =====================================================================
; Drag logic: free drag -> release detection -> snap to bounds if needed
; =====================================================================
Drag(neutron) {
; 1. Let system handle dragging for smooth tracking experience
PostMessage 0xA1, 2, 0, , "ahk_id " neutron.hWnd
; 2. Wait for left mouse button release (drag ends)
KeyWait "LButton"
; 3. After drag ends, detect position and snap to bounds if needed
CheckAndSnap(neutron)
}
CheckAndSnap(neutron) {
try {
; Get current window position and size
WinGetPos &x, &y, &w, &h, "ahk_id " neutron.hWnd
; Get work area of primary monitor (automatically excludes taskbar height)
; WALeft, WATop, WARight, WABottom are left, top, right, bottom boundaries of work area
MonitorGetWorkArea 1, &WALeft, &WATop, &WARight, &WABottom
; Record target coordinates, default to keeping current position
targetX := x
targetY := y
needsFix := false
; --- 1. Horizontal boundary check ---
; Logic: if window's right edge (x + w) exceeds screen's right edge (WARight)
; it means window is partially off the right side of screen
if (x + w > WARight) {
; Fix: snap window to right edge
targetX := WARight - w
needsFix := true
}
; --- 2. Vertical boundary check ---
; Logic: if window's bottom edge (y + h) exceeds taskbar top edge (WABottom)
; it means window is partially below the taskbar or off-screen
if (y + h > WABottom) {
; Fix: snap window to above taskbar
targetY := WABottom - h
needsFix := true
}
; --- Apply fix ---
if (needsFix) {
; Use WinMove to reposition window to corrected coordinates
WinMove targetX, targetY, , , "ahk_id " neutron.hWnd
}
} catch {
; Ignore potential handle errors
}
}
; Optional: JS can call this to sync its own state back to AHK.
; Example payload: {collapsed:true, blocked:false}
SyncFromUi(neutron, payload) {
; You can keep this empty for MVP.
; It exists so your front-end can evolve without breaking.
}
; =====================================================================
; Actions
; =====================================================================
ToggleCollapsed() {
global gCollapsed, UI_W_EXPANDED, UI_W_COLLAPSED, UI_H, neutron
gCollapsed := !gCollapsed
; Option A: Only let the front-end animate/hide panel, keep window same.
; Just update state and inform UI.
PushStateToUI()
; Option B (recommended): actually resize window so only arrow remains.
; This makes it feel like ToDesk: only one arrow left when collapsed.
w := gCollapsed ? UI_W_COLLAPSED : UI_W_EXPANDED
WinGetPos &currX, &currY, &currW, &currH, "ahk_id " neutron.hWnd
currentRightEdge := currX + currW
x := currentRightEdge - W
try {
WinMove x, currY, w, UI_H, "ahk_id " neutron.hWnd
} catch {
; ignore if hWnd not available
}
}
ToggleKeyboardBlock() {
global gKbdBlocked
gKbdBlocked := !gKbdBlocked
if gKbdBlocked
EnableKeyboardBlock()
else
DisableKeyboardBlock()
PushStateToUI()
}
EmergencyUnblock() {
global gKbdBlocked
if gKbdBlocked {
gKbdBlocked := false
DisableKeyboardBlock()
PushStateToUI()
SoundBeep 880, 80
} else {
; If already unblocked, you can choose to exit quickly:
; ExitProc()
SoundBeep 660, 60
}
}
ExitProc() {
global gKbdBlocked
if gKbdBlocked {
gKbdBlocked := false
DisableKeyboardBlock()
}
ExitApp
}
; =====================================================================
; Keyboard block implementation (mouse unaffected)
; Strategy: swallow most keys via Hotkeys, keep emergency hotkey alive.
; =====================================================================
EnableKeyboardBlock() {
; Swallow most keyboard input.
; We intentionally do NOT block mouse.
; Keep Ctrl+Alt+Backspace active as emergency - don't block it.
for key in GetBlockKeyList() {
; Skip Backspace - we need Ctrl+Alt+Backspace to work
if (key = "Backspace")
continue
try Hotkey "*" key, Swallow, "On"
}
; Block Backspace without * modifier so Ctrl+Alt+BS still works
try Hotkey "Backspace", Swallow, "On"
}
DisableKeyboardBlock() {
for key in GetBlockKeyList() {
if (key = "Backspace") {
try Hotkey "Backspace", "Off"
} else {
try Hotkey "*" key, "Off"
}
}
}
Swallow(*) {
; Do nothing; hotkey fires and prevents the key from reaching apps.
return
}
GetBlockKeyList() {
; Covers letters, digits, function keys, arrows, editing keys, etc.
; You can adjust as needed.
static keys := []
if keys.Length
return keys
; Letters
Loop 26 {
keys.Push(Chr(Ord("A") + A_Index - 1))
}
; Digits
Loop 10 {
keys.Push("" (A_Index - 1))
}
; Function keys
Loop 24 {
keys.Push("F" A_Index)
}
; Editing/navigation
for k in [
"Tab","Enter","Space","Backspace","Delete","Insert",
"Home","End","PgUp","PgDn",
"Up","Down","Left","Right",
"Esc",
"CapsLock","NumLock","ScrollLock",
"PrintScreen","Pause"
]
keys.Push(k)
; Numpad
for k in [
"Numpad0","Numpad1","Numpad2","Numpad3","Numpad4",
"Numpad5","Numpad6","Numpad7","Numpad8","Numpad9",
"NumpadDot","NumpadDiv","NumpadMult","NumpadAdd","NumpadSub","NumpadEnter"
]
keys.Push(k)
; Punctuation / symbols (US layout, still safe to include)
for k in [
"`-","`=","`[","`]", "`\","`;","`'","`,","`.","`/",
"AppsKey"
]
keys.Push(k)
; Modifiers: we generally DO swallow them too to prevent any accidental shortcuts,
; but leaving them enabled can be useful. Choose your preference.
; If you swallow modifiers, mouse-only still works, but hotkeys won't.
; for k in ["LShift","RShift","LControl","RControl","LAlt","RAlt","LWin","RWin"]
; keys.Push(k)
return keys
}
; =====================================================================
; Position helpers: bottom-right above taskbar
; =====================================================================
GetDockX(w) {
global MARGIN_RIGHT
; Use the work area so it sits above the taskbar automatically
MonitorGetWorkArea 1, &l, &t, &r, &b
return r - w - MARGIN_RIGHT
}
GetDockY(h) {
global MARGIN_BOTTOM
MonitorGetWorkArea 1, &l, &t, &r, &b
return b - h - MARGIN_BOTTOM
}
; =====================================================================
; Push state to UI (optional but nice)
; Your HTML can implement window.setState({collapsed, blocked})
; =====================================================================
PushStateToUI() {
global neutron, gCollapsed, gKbdBlocked
if !neutron
return
; Use neutron.wnd to access the JavaScript window object directly
try {
neutron.wnd.setState(ComObject(0x400B, ComObjArray(0xC, 2, gCollapsed, gKbdBlocked)))
} catch {
; Alternative: call execScript to run JS code
try {
js := "if(window.setState){window.setState({collapsed:" (gCollapsed ? "true" : "false") ",blocked:" (gKbdBlocked ? "true" : "false") "});}"
neutron.wnd.execScript(js, "javascript")
} catch {
; ignore if not supported
}
}
}