Repository navigation
Expand file tree
/
Copy pathFNSModifiers.py
More file actions
195 lines (162 loc) · 6.46 KB
/
Copy pathFNSModifiers.py
File metadata and controls
195 lines (162 loc) · 6.46 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
"""Authoritative modifier-key state, shared by the tools that need it.
WHY THIS EXISTS
---------------
TouchDesigner's Keyboard In CHOP latches. The app loses focus while a
modifier is down, the keyup event is never delivered, and the channel stays
at 1 until that key is pressed and released again. Alt-tabbing is the usual
cause and macOS is markedly worse. The stickiness is in TD's event
integration, so it cannot be fixed at the CHOP layer.
WHAT TD OFFERS INSTEAD (measured 2026-09-01, TD 2025.33070)
-----------------------------------------------------------
Nothing usable for this. `ui` exposes no modifier state at all -- the full
attribute list has rollover/rolloverOp/rolloverPar/rolloverParGroup and
nothing for keys. TD's panel values DO carry `ctrl` / `alt` / `shift` /
`cmd`, but the documented semantics are "1 if the key is down WHEN THE
PANEL IS CLICKED ON": they are latched at click time. That is immune to a
missed keyup, but it cannot answer "is shift held right now" anywhere
except inside a click on that specific panel -- and all three callers read
outside a click (a hotkey handler, a drop handler, and a display refresh).
So panel values are the right idea with the wrong lifetime.
THE SEAM
--------
Callers use `held('shift')`. Everything below it is strategy, kept in ONE
file precisely so it can be changed without touching three tools again.
The alternatives considered were: timeout decay (clear a modifier held
longer than N seconds -- crude, cannot tell a genuinely-held key from a
stuck one) and reconcile-on-next-event (cheap, but does not end the stuck
window, only waits for the user to end it).
FALLBACK IS THE CONTRACT
------------------------
`held()` returns True, False, or **None meaning "cannot tell"** -- an
unsupported platform, a library that will not load, any exception. Callers
treat None as "fall back to whatever you read before", so a platform where
this does not work behaves EXACTLY as it does today. It never fails open
(inventing a held modifier) and never fails shut (denying a real one).
macOS is IMPLEMENTED BUT UNVERIFIED: it was written from the
ApplicationServices API and could not be tested from Windows. If it is
wrong it raises, `held()` returns None, and the Mac keeps its current
behaviour -- which is the whole reason the fallback is shaped this way.
"""
import sys
# Windows virtual-key codes. Left/right variants are folded together: a
# caller asking "is shift held" does not care which shift.
_VK = {
'shift': (0x10,),
'ctrl': (0x11,),
'alt': (0x12,),
'cmd': (0x5B, 0x5C), # left/right Win key, the Windows analogue
}
# macOS CGEventFlags bits (ApplicationServices/CoreGraphics).
_CG_FLAG = {
'shift': 1 << 17,
'ctrl': 1 << 18,
'alt': 1 << 19, # Option
'cmd': 1 << 20,
}
_NAMES = ('shift', 'ctrl', 'alt', 'cmd')
_mac_source = None # cached CGEventSourceFlagsState, or False once failed
def _heldWindows(name):
import ctypes
codes = _VK.get(name)
if not codes:
return None
gaks = ctypes.windll.user32.GetAsyncKeyState
# high bit = currently down; the low bit is "pressed since last call"
# and must NOT be used -- it would report a key the user already released.
return any(bool(gaks(c) & 0x8000) for c in codes)
def _heldMac(name):
"""macOS via CGEventSourceFlagsState. UNVERIFIED -- see module docstring."""
global _mac_source
bit = _CG_FLAG.get(name)
if not bit:
return None
if _mac_source is False:
return None
if _mac_source is None:
import ctypes, ctypes.util
path = ctypes.util.find_library('ApplicationServices')
if not path:
_mac_source = False
return None
lib = ctypes.cdll.LoadLibrary(path)
fn = lib.CGEventSourceFlagsState
fn.restype = ctypes.c_uint64
fn.argtypes = [ctypes.c_uint32]
_mac_source = fn
# 1 = kCGEventSourceStateCombinedSessionState
return bool(_mac_source(1) & bit)
def held(name):
"""True / False / None for one modifier, asked of the OS right now.
None means CANNOT TELL, and is the caller's signal to fall back to
whatever it read before. Never raises.
"""
key = str(name or '').strip().lower()
if key not in _NAMES:
return None
try:
if sys.platform == 'win32':
return _heldWindows(key)
if sys.platform == 'darwin':
return _heldMac(key)
except Exception:
return None
return None
def heldOr(name, fallback):
"""`held(name)`, or `fallback` when the OS cannot be asked.
The shape every caller wants: one expression that is authoritative
where it can be and unchanged where it cannot.
"""
value = held(name)
return bool(fallback) if value is None else value
# Mouse buttons. TD has no "is a button down right now" either: panel
# values (lselect...) only count presses that START on that panel, so a
# drag that began elsewhere -- a parameter dragged in from the parameter
# dialog -- shows as inside=1 with every select at 0 (measured 2026-10-07,
# TD 2025.33070, and dragrollover never fired for a legacy-drop panel).
_VK_BUTTON = {'left': 0x01, 'right': 0x02, 'middle': 0x04}
_CG_BUTTON = {'left': 0, 'right': 1, 'middle': 2}
_mac_buttons = None # cached CGEventSourceButtonState, or False once failed
def _buttonWindows(name):
import ctypes
code = _VK_BUTTON.get(name)
if code is None:
return None
# physical buttons: a left-handed swap is not applied here
return bool(ctypes.windll.user32.GetAsyncKeyState(code) & 0x8000)
def _buttonMac(name):
"""macOS via CGEventSourceButtonState. UNVERIFIED -- see module docstring."""
global _mac_buttons
button = _CG_BUTTON.get(name)
if button is None or _mac_buttons is False:
return None
if _mac_buttons is None:
import ctypes, ctypes.util
path = ctypes.util.find_library('ApplicationServices')
if not path:
_mac_buttons = False
return None
fn = ctypes.cdll.LoadLibrary(path).CGEventSourceButtonState
fn.restype = ctypes.c_bool
fn.argtypes = [ctypes.c_uint32, ctypes.c_uint32]
_mac_buttons = fn
return bool(_mac_buttons(1, button))
def buttonHeld(name='left'):
"""True / False / None for a mouse button ('left', 'right', 'middle'),
asked of the OS right now. None means CANNOT TELL. Never raises."""
key = str(name or '').strip().lower()
if key not in _VK_BUTTON:
return None
try:
if sys.platform == 'win32':
return _buttonWindows(key)
if sys.platform == 'darwin':
return _buttonMac(key)
except Exception:
return None
return None
def state():
"""All four modifiers at once, for diagnostics. None where unknown."""
return {n: held(n) for n in _NAMES}
def available():
"""Whether this platform can answer at all -- for a status readout."""
return held('shift') is not None