prkit 0.1.0__py3-none-any.whl
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.
- prkit/__init__.py +3 -0
- prkit/__main__.py +3 -0
- prkit/app.py +183 -0
- prkit/cursor_watchdog.py +48 -0
- prkit/icon.py +25 -0
- prkit/keyboard.py +68 -0
- prkit/keys.py +111 -0
- prkit/mac_keyboard.py +240 -0
- prkit/overlays.py +170 -0
- prkit/platform.py +152 -0
- prkit/py.typed +0 -0
- prkit/settings.py +102 -0
- prkit/spotlight.py +26 -0
- prkit/tray.py +242 -0
- prkit/windows_cursor.py +467 -0
- prkit-0.1.0.dist-info/METADATA +100 -0
- prkit-0.1.0.dist-info/RECORD +19 -0
- prkit-0.1.0.dist-info/WHEEL +4 -0
- prkit-0.1.0.dist-info/entry_points.txt +4 -0
prkit/windows_cursor.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from ctypes import wintypes
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from prkit.cursor_watchdog import restore_system_cursors
|
|
14
|
+
from prkit.settings import SpotlightSettings
|
|
15
|
+
|
|
16
|
+
DIB_RGB_COLORS = 0
|
|
17
|
+
DI_NORMAL = 0x0003
|
|
18
|
+
BI_RGB = 0
|
|
19
|
+
IMAGE_CURSOR = 2
|
|
20
|
+
SPI_SETCURSORS = 0x0057
|
|
21
|
+
CREATE_NO_WINDOW = 0x08000000
|
|
22
|
+
|
|
23
|
+
SYSTEM_CURSOR_IDS = (
|
|
24
|
+
32512, # OCR_NORMAL
|
|
25
|
+
32513, # OCR_IBEAM
|
|
26
|
+
32514, # OCR_WAIT
|
|
27
|
+
32515, # OCR_CROSS
|
|
28
|
+
32516, # OCR_UP
|
|
29
|
+
32642, # OCR_SIZENWSE
|
|
30
|
+
32643, # OCR_SIZENESW
|
|
31
|
+
32644, # OCR_SIZEWE
|
|
32
|
+
32645, # OCR_SIZENS
|
|
33
|
+
32646, # OCR_SIZEALL
|
|
34
|
+
32648, # OCR_NO
|
|
35
|
+
32649, # OCR_HAND
|
|
36
|
+
32650, # OCR_APPSTARTING
|
|
37
|
+
32651, # OCR_HELP
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CursorBackendError(RuntimeError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ICONINFO(ctypes.Structure):
|
|
46
|
+
_fields_ = [
|
|
47
|
+
("fIcon", wintypes.BOOL),
|
|
48
|
+
("xHotspot", wintypes.DWORD),
|
|
49
|
+
("yHotspot", wintypes.DWORD),
|
|
50
|
+
("hbmMask", wintypes.HBITMAP),
|
|
51
|
+
("hbmColor", wintypes.HBITMAP),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BITMAPINFOHEADER(ctypes.Structure):
|
|
56
|
+
_fields_ = [
|
|
57
|
+
("biSize", wintypes.DWORD),
|
|
58
|
+
("biWidth", wintypes.LONG),
|
|
59
|
+
("biHeight", wintypes.LONG),
|
|
60
|
+
("biPlanes", wintypes.WORD),
|
|
61
|
+
("biBitCount", wintypes.WORD),
|
|
62
|
+
("biCompression", wintypes.DWORD),
|
|
63
|
+
("biSizeImage", wintypes.DWORD),
|
|
64
|
+
("biXPelsPerMeter", wintypes.LONG),
|
|
65
|
+
("biYPelsPerMeter", wintypes.LONG),
|
|
66
|
+
("biClrUsed", wintypes.DWORD),
|
|
67
|
+
("biClrImportant", wintypes.DWORD),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RGBQUAD(ctypes.Structure):
|
|
72
|
+
_fields_ = [
|
|
73
|
+
("rgbBlue", ctypes.c_ubyte),
|
|
74
|
+
("rgbGreen", ctypes.c_ubyte),
|
|
75
|
+
("rgbRed", ctypes.c_ubyte),
|
|
76
|
+
("rgbReserved", ctypes.c_ubyte),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BITMAPINFO(ctypes.Structure):
|
|
81
|
+
_fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", RGBQUAD * 1)]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MONO_BITMAPINFO(ctypes.Structure):
|
|
85
|
+
_fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", RGBQUAD * 2)]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class BITMAP(ctypes.Structure):
|
|
89
|
+
_fields_ = [
|
|
90
|
+
("bmType", wintypes.LONG),
|
|
91
|
+
("bmWidth", wintypes.LONG),
|
|
92
|
+
("bmHeight", wintypes.LONG),
|
|
93
|
+
("bmWidthBytes", wintypes.LONG),
|
|
94
|
+
("bmPlanes", wintypes.WORD),
|
|
95
|
+
("bmBitsPixel", wintypes.WORD),
|
|
96
|
+
("bmBits", ctypes.c_void_p),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CursorWatchdog:
|
|
101
|
+
def __init__(self) -> None:
|
|
102
|
+
self._kernel32 = ctypes.windll.kernel32
|
|
103
|
+
self._kernel32.CreateEventW.restype = wintypes.HANDLE
|
|
104
|
+
self._event_name = f"Local\\PrtoolsCursorRestore-{uuid.uuid4()}"
|
|
105
|
+
self._event = self._kernel32.CreateEventW(None, True, False, self._event_name)
|
|
106
|
+
if not self._event:
|
|
107
|
+
raise CursorBackendError("無法建立游標復原事件")
|
|
108
|
+
try:
|
|
109
|
+
self._process = subprocess.Popen(
|
|
110
|
+
self._command(),
|
|
111
|
+
creationflags=CREATE_NO_WINDOW,
|
|
112
|
+
close_fds=True,
|
|
113
|
+
)
|
|
114
|
+
time.sleep(0.05)
|
|
115
|
+
if self._process.poll() is not None:
|
|
116
|
+
raise CursorBackendError("游標復原監控程序啟動失敗")
|
|
117
|
+
except Exception:
|
|
118
|
+
self._kernel32.CloseHandle(self._event)
|
|
119
|
+
self._event = None
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
def _command(self) -> list[str]:
|
|
123
|
+
sibling = Path(sys.executable).with_name("prkit-cursor-watchdog.exe")
|
|
124
|
+
if sibling.is_file():
|
|
125
|
+
return [str(sibling), str(os.getpid()), self._event_name]
|
|
126
|
+
if "__compiled__" in globals():
|
|
127
|
+
raise CursorBackendError("找不到 prkit-cursor-watchdog.exe")
|
|
128
|
+
pythonw = Path(sys.executable).with_name("pythonw.exe")
|
|
129
|
+
executable = pythonw if pythonw.is_file() else Path(sys.executable)
|
|
130
|
+
return [
|
|
131
|
+
str(executable),
|
|
132
|
+
"-m",
|
|
133
|
+
"prkit.cursor_watchdog",
|
|
134
|
+
str(os.getpid()),
|
|
135
|
+
self._event_name,
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
def stop(self) -> None:
|
|
139
|
+
if not self._event:
|
|
140
|
+
return
|
|
141
|
+
self._kernel32.SetEvent(self._event)
|
|
142
|
+
try:
|
|
143
|
+
self._process.wait(timeout=2)
|
|
144
|
+
except subprocess.TimeoutExpired:
|
|
145
|
+
self._process.terminate()
|
|
146
|
+
self._kernel32.CloseHandle(self._event)
|
|
147
|
+
self._event = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class WindowsCursorSpotlight:
|
|
151
|
+
def __init__(self, settings: SpotlightSettings) -> None:
|
|
152
|
+
self._color = settings.color
|
|
153
|
+
self._opacity = settings.opacity
|
|
154
|
+
self._diameter = settings.size
|
|
155
|
+
self._enabled = False
|
|
156
|
+
self._watchdog: CursorWatchdog | None = None
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def diameter(self) -> int:
|
|
160
|
+
return self._diameter
|
|
161
|
+
|
|
162
|
+
def set_enabled(self, enabled: bool) -> None:
|
|
163
|
+
if enabled == self._enabled:
|
|
164
|
+
return
|
|
165
|
+
if enabled:
|
|
166
|
+
watchdog = CursorWatchdog()
|
|
167
|
+
try:
|
|
168
|
+
self._replace_cursors()
|
|
169
|
+
except Exception:
|
|
170
|
+
restore_system_cursors()
|
|
171
|
+
watchdog.stop()
|
|
172
|
+
raise
|
|
173
|
+
self._watchdog = watchdog
|
|
174
|
+
self._enabled = True
|
|
175
|
+
else:
|
|
176
|
+
restore_system_cursors()
|
|
177
|
+
self._enabled = False
|
|
178
|
+
if self._watchdog:
|
|
179
|
+
self._watchdog.stop()
|
|
180
|
+
self._watchdog = None
|
|
181
|
+
|
|
182
|
+
def sync_position(self) -> None:
|
|
183
|
+
"""The Windows implementation is the system cursor and needs no syncing."""
|
|
184
|
+
|
|
185
|
+
def set_appearance(self, color: str, opacity: int, diameter: int) -> None:
|
|
186
|
+
self._color = color
|
|
187
|
+
self._opacity = opacity
|
|
188
|
+
self._diameter = diameter
|
|
189
|
+
if self._enabled:
|
|
190
|
+
restore_system_cursors()
|
|
191
|
+
try:
|
|
192
|
+
self._replace_cursors()
|
|
193
|
+
except Exception:
|
|
194
|
+
restore_system_cursors()
|
|
195
|
+
self._enabled = False
|
|
196
|
+
if self._watchdog:
|
|
197
|
+
self._watchdog.stop()
|
|
198
|
+
self._watchdog = None
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
def _replace_cursors(self) -> None:
|
|
202
|
+
user32 = ctypes.windll.user32
|
|
203
|
+
user32.LoadCursorW.restype = wintypes.HANDLE
|
|
204
|
+
user32.LoadCursorW.argtypes = [wintypes.HINSTANCE, ctypes.c_void_p]
|
|
205
|
+
user32.SetSystemCursor.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
206
|
+
user32.SetSystemCursor.restype = wintypes.BOOL
|
|
207
|
+
user32.DestroyCursor.argtypes = [wintypes.HANDLE]
|
|
208
|
+
originals: list[tuple[int, int]] = []
|
|
209
|
+
for cursor_id in SYSTEM_CURSOR_IDS:
|
|
210
|
+
cursor = user32.LoadCursorW(None, ctypes.c_void_p(cursor_id))
|
|
211
|
+
if cursor:
|
|
212
|
+
originals.append((cursor_id, cursor))
|
|
213
|
+
custom: list[tuple[int, int]] = []
|
|
214
|
+
owned_handles: set[int] = set()
|
|
215
|
+
try:
|
|
216
|
+
for cursor_id, original in originals:
|
|
217
|
+
cursor = _create_spotlight_cursor(
|
|
218
|
+
original, self._color, self._opacity, self._diameter
|
|
219
|
+
)
|
|
220
|
+
custom.append((cursor_id, cursor))
|
|
221
|
+
owned_handles.add(cursor)
|
|
222
|
+
for cursor_id, cursor in custom:
|
|
223
|
+
if not user32.SetSystemCursor(cursor, cursor_id):
|
|
224
|
+
raise CursorBackendError(f"無法替換系統游標 {cursor_id}")
|
|
225
|
+
owned_handles.remove(cursor)
|
|
226
|
+
except Exception:
|
|
227
|
+
for cursor in owned_handles:
|
|
228
|
+
user32.DestroyCursor(cursor)
|
|
229
|
+
raise
|
|
230
|
+
if not custom:
|
|
231
|
+
raise CursorBackendError("找不到可替換的 Windows 系統游標")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _create_spotlight_cursor(original: int, color: str, opacity: int, size: int) -> int:
|
|
235
|
+
user32 = ctypes.windll.user32
|
|
236
|
+
gdi32 = ctypes.windll.gdi32
|
|
237
|
+
user32.GetIconInfo.argtypes = [wintypes.HANDLE, ctypes.POINTER(ICONINFO)]
|
|
238
|
+
user32.GetIconInfo.restype = wintypes.BOOL
|
|
239
|
+
user32.GetDC.argtypes = [wintypes.HWND]
|
|
240
|
+
user32.GetDC.restype = wintypes.HDC
|
|
241
|
+
user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC]
|
|
242
|
+
user32.DrawIconEx.argtypes = [
|
|
243
|
+
wintypes.HDC,
|
|
244
|
+
ctypes.c_int,
|
|
245
|
+
ctypes.c_int,
|
|
246
|
+
wintypes.HANDLE,
|
|
247
|
+
ctypes.c_int,
|
|
248
|
+
ctypes.c_int,
|
|
249
|
+
wintypes.UINT,
|
|
250
|
+
wintypes.HBRUSH,
|
|
251
|
+
wintypes.UINT,
|
|
252
|
+
]
|
|
253
|
+
user32.CreateIconIndirect.argtypes = [ctypes.POINTER(ICONINFO)]
|
|
254
|
+
user32.CreateIconIndirect.restype = wintypes.HANDLE
|
|
255
|
+
gdi32.CreateDIBSection.restype = wintypes.HBITMAP
|
|
256
|
+
gdi32.CreateDIBSection.argtypes = [
|
|
257
|
+
wintypes.HDC,
|
|
258
|
+
ctypes.POINTER(BITMAPINFO),
|
|
259
|
+
wintypes.UINT,
|
|
260
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
261
|
+
wintypes.HANDLE,
|
|
262
|
+
wintypes.DWORD,
|
|
263
|
+
]
|
|
264
|
+
gdi32.CreateBitmap.restype = wintypes.HBITMAP
|
|
265
|
+
gdi32.CreateBitmap.argtypes = [
|
|
266
|
+
ctypes.c_int,
|
|
267
|
+
ctypes.c_int,
|
|
268
|
+
wintypes.UINT,
|
|
269
|
+
wintypes.UINT,
|
|
270
|
+
ctypes.c_void_p,
|
|
271
|
+
]
|
|
272
|
+
gdi32.CreateCompatibleDC.restype = wintypes.HDC
|
|
273
|
+
gdi32.CreateCompatibleDC.argtypes = [wintypes.HDC]
|
|
274
|
+
gdi32.SelectObject.restype = wintypes.HANDLE
|
|
275
|
+
gdi32.SelectObject.argtypes = [wintypes.HDC, wintypes.HANDLE]
|
|
276
|
+
gdi32.DeleteDC.argtypes = [wintypes.HDC]
|
|
277
|
+
gdi32.DeleteObject.argtypes = [wintypes.HANDLE]
|
|
278
|
+
gdi32.GetObjectW.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p]
|
|
279
|
+
gdi32.GetDIBits.argtypes = [
|
|
280
|
+
wintypes.HDC,
|
|
281
|
+
wintypes.HBITMAP,
|
|
282
|
+
wintypes.UINT,
|
|
283
|
+
wintypes.UINT,
|
|
284
|
+
ctypes.c_void_p,
|
|
285
|
+
ctypes.c_void_p,
|
|
286
|
+
wintypes.UINT,
|
|
287
|
+
]
|
|
288
|
+
icon_info = ICONINFO()
|
|
289
|
+
if not user32.GetIconInfo(original, ctypes.byref(icon_info)):
|
|
290
|
+
raise CursorBackendError("無法讀取原始游標")
|
|
291
|
+
try:
|
|
292
|
+
bitmap_info = BITMAPINFO()
|
|
293
|
+
bitmap_info.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
|
294
|
+
bitmap_info.bmiHeader.biWidth = size
|
|
295
|
+
bitmap_info.bmiHeader.biHeight = -size
|
|
296
|
+
bitmap_info.bmiHeader.biPlanes = 1
|
|
297
|
+
bitmap_info.bmiHeader.biBitCount = 32
|
|
298
|
+
bitmap_info.bmiHeader.biCompression = BI_RGB
|
|
299
|
+
bits = ctypes.c_void_p()
|
|
300
|
+
screen_dc = user32.GetDC(None)
|
|
301
|
+
color_bitmap = gdi32.CreateDIBSection(
|
|
302
|
+
screen_dc,
|
|
303
|
+
ctypes.byref(bitmap_info),
|
|
304
|
+
DIB_RGB_COLORS,
|
|
305
|
+
ctypes.byref(bits),
|
|
306
|
+
None,
|
|
307
|
+
0,
|
|
308
|
+
)
|
|
309
|
+
user32.ReleaseDC(None, screen_dc)
|
|
310
|
+
if not color_bitmap or not bits.value:
|
|
311
|
+
raise CursorBackendError("無法建立游標圖像")
|
|
312
|
+
mask_bitmap = gdi32.CreateBitmap(size, size, 1, 1, None)
|
|
313
|
+
if not mask_bitmap:
|
|
314
|
+
gdi32.DeleteObject(color_bitmap)
|
|
315
|
+
raise CursorBackendError("無法建立游標遮罩")
|
|
316
|
+
try:
|
|
317
|
+
_paint_circle(bits.value, color, opacity, size)
|
|
318
|
+
if icon_info.hbmColor:
|
|
319
|
+
memory_dc = gdi32.CreateCompatibleDC(None)
|
|
320
|
+
previous = gdi32.SelectObject(memory_dc, color_bitmap)
|
|
321
|
+
try:
|
|
322
|
+
user32.DrawIconEx(
|
|
323
|
+
memory_dc,
|
|
324
|
+
size // 2 - icon_info.xHotspot,
|
|
325
|
+
size // 2 - icon_info.yHotspot,
|
|
326
|
+
original,
|
|
327
|
+
0,
|
|
328
|
+
0,
|
|
329
|
+
0,
|
|
330
|
+
None,
|
|
331
|
+
DI_NORMAL,
|
|
332
|
+
)
|
|
333
|
+
finally:
|
|
334
|
+
gdi32.SelectObject(memory_dc, previous)
|
|
335
|
+
gdi32.DeleteDC(memory_dc)
|
|
336
|
+
else:
|
|
337
|
+
_paint_monochrome_cursor(
|
|
338
|
+
bits.value,
|
|
339
|
+
icon_info.hbmMask,
|
|
340
|
+
icon_info.xHotspot,
|
|
341
|
+
icon_info.yHotspot,
|
|
342
|
+
size,
|
|
343
|
+
)
|
|
344
|
+
combined = ICONINFO(
|
|
345
|
+
False,
|
|
346
|
+
size // 2,
|
|
347
|
+
size // 2,
|
|
348
|
+
mask_bitmap,
|
|
349
|
+
color_bitmap,
|
|
350
|
+
)
|
|
351
|
+
cursor = user32.CreateIconIndirect(ctypes.byref(combined))
|
|
352
|
+
if not cursor:
|
|
353
|
+
raise CursorBackendError("無法建立合成游標")
|
|
354
|
+
return cursor
|
|
355
|
+
finally:
|
|
356
|
+
gdi32.DeleteObject(mask_bitmap)
|
|
357
|
+
gdi32.DeleteObject(color_bitmap)
|
|
358
|
+
finally:
|
|
359
|
+
if icon_info.hbmMask:
|
|
360
|
+
gdi32.DeleteObject(icon_info.hbmMask)
|
|
361
|
+
if icon_info.hbmColor:
|
|
362
|
+
gdi32.DeleteObject(icon_info.hbmColor)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _paint_circle(address: int, color: str, opacity: int, size: int) -> None:
|
|
366
|
+
rgb = int(color.removeprefix("#"), 16)
|
|
367
|
+
red, green, blue = (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF
|
|
368
|
+
maximum_alpha = round(255 * opacity / 100)
|
|
369
|
+
radius = size / 2 - 1
|
|
370
|
+
center = (size - 1) / 2
|
|
371
|
+
pixels = (ctypes.c_uint32 * (size * size)).from_address(address)
|
|
372
|
+
for y in range(size):
|
|
373
|
+
for x in range(size):
|
|
374
|
+
distance = math.hypot(x - center, y - center)
|
|
375
|
+
coverage = max(0.0, min(1.0, radius + 0.75 - distance))
|
|
376
|
+
alpha = round(maximum_alpha * coverage)
|
|
377
|
+
premultiplied_red = red * alpha // 255
|
|
378
|
+
premultiplied_green = green * alpha // 255
|
|
379
|
+
premultiplied_blue = blue * alpha // 255
|
|
380
|
+
pixels[y * size + x] = (
|
|
381
|
+
alpha << 24
|
|
382
|
+
| premultiplied_red << 16
|
|
383
|
+
| premultiplied_green << 8
|
|
384
|
+
| premultiplied_blue
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _paint_monochrome_cursor(
|
|
389
|
+
destination: int,
|
|
390
|
+
mask_bitmap: int,
|
|
391
|
+
hotspot_x: int,
|
|
392
|
+
hotspot_y: int,
|
|
393
|
+
destination_size: int,
|
|
394
|
+
) -> int:
|
|
395
|
+
"""Composite a monochrome AND/XOR cursor mask without its bounding box."""
|
|
396
|
+
user32 = ctypes.windll.user32
|
|
397
|
+
gdi32 = ctypes.windll.gdi32
|
|
398
|
+
user32.GetDC.argtypes = [wintypes.HWND]
|
|
399
|
+
user32.GetDC.restype = wintypes.HDC
|
|
400
|
+
user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC]
|
|
401
|
+
gdi32.GetObjectW.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p]
|
|
402
|
+
gdi32.GetDIBits.argtypes = [
|
|
403
|
+
wintypes.HDC,
|
|
404
|
+
wintypes.HBITMAP,
|
|
405
|
+
wintypes.UINT,
|
|
406
|
+
wintypes.UINT,
|
|
407
|
+
ctypes.c_void_p,
|
|
408
|
+
ctypes.c_void_p,
|
|
409
|
+
wintypes.UINT,
|
|
410
|
+
]
|
|
411
|
+
bitmap = BITMAP()
|
|
412
|
+
if not gdi32.GetObjectW(mask_bitmap, ctypes.sizeof(bitmap), ctypes.byref(bitmap)):
|
|
413
|
+
raise CursorBackendError("無法讀取單色游標遮罩")
|
|
414
|
+
cursor_width = bitmap.bmWidth
|
|
415
|
+
cursor_height = bitmap.bmHeight // 2
|
|
416
|
+
mask_height = cursor_height * 2
|
|
417
|
+
stride = ((cursor_width + 31) // 32) * 4
|
|
418
|
+
mask_bytes = (ctypes.c_ubyte * (stride * mask_height))()
|
|
419
|
+
bitmap_info = MONO_BITMAPINFO()
|
|
420
|
+
bitmap_info.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
|
421
|
+
bitmap_info.bmiHeader.biWidth = cursor_width
|
|
422
|
+
bitmap_info.bmiHeader.biHeight = -mask_height
|
|
423
|
+
bitmap_info.bmiHeader.biPlanes = 1
|
|
424
|
+
bitmap_info.bmiHeader.biBitCount = 1
|
|
425
|
+
bitmap_info.bmiHeader.biCompression = BI_RGB
|
|
426
|
+
bitmap_info.bmiColors[1] = RGBQUAD(255, 255, 255, 0)
|
|
427
|
+
screen_dc = user32.GetDC(None)
|
|
428
|
+
try:
|
|
429
|
+
rows = gdi32.GetDIBits(
|
|
430
|
+
screen_dc,
|
|
431
|
+
mask_bitmap,
|
|
432
|
+
0,
|
|
433
|
+
mask_height,
|
|
434
|
+
mask_bytes,
|
|
435
|
+
ctypes.byref(bitmap_info),
|
|
436
|
+
DIB_RGB_COLORS,
|
|
437
|
+
)
|
|
438
|
+
finally:
|
|
439
|
+
user32.ReleaseDC(None, screen_dc)
|
|
440
|
+
if rows != mask_height:
|
|
441
|
+
raise CursorBackendError("無法擷取單色游標遮罩")
|
|
442
|
+
|
|
443
|
+
def bit_at(x: int, y: int) -> int:
|
|
444
|
+
return (mask_bytes[y * stride + x // 8] >> (7 - x % 8)) & 1
|
|
445
|
+
|
|
446
|
+
pixels = (ctypes.c_uint32 * (destination_size * destination_size)).from_address(destination)
|
|
447
|
+
origin_x = destination_size // 2 - hotspot_x
|
|
448
|
+
origin_y = destination_size // 2 - hotspot_y
|
|
449
|
+
painted = 0
|
|
450
|
+
for cursor_y in range(cursor_height):
|
|
451
|
+
target_y = origin_y + cursor_y
|
|
452
|
+
if not 0 <= target_y < destination_size:
|
|
453
|
+
continue
|
|
454
|
+
for cursor_x in range(cursor_width):
|
|
455
|
+
target_x = origin_x + cursor_x
|
|
456
|
+
if not 0 <= target_x < destination_size:
|
|
457
|
+
continue
|
|
458
|
+
and_bit = bit_at(cursor_x, cursor_y)
|
|
459
|
+
xor_bit = bit_at(cursor_x, cursor_y + cursor_height)
|
|
460
|
+
if and_bit and not xor_bit:
|
|
461
|
+
continue
|
|
462
|
+
# Fixed black/white pixels replace XOR inversion. The translucent
|
|
463
|
+
# spotlight beneath them provides a predictable contrast surface.
|
|
464
|
+
pixel = 0xFFFFFFFF if xor_bit and not and_bit else 0xFF000000
|
|
465
|
+
pixels[target_y * destination_size + target_x] = pixel
|
|
466
|
+
painted += 1
|
|
467
|
+
return painted
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: prkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A cross-platform system-tray presentation toolkit
|
|
5
|
+
Author: meebox
|
|
6
|
+
Author-email: meebox <meebox@gmail.com>
|
|
7
|
+
Requires-Dist: pynput>=1.8.2
|
|
8
|
+
Requires-Dist: pyobjc-framework-cocoa>=12.2.1 ; sys_platform == 'darwin'
|
|
9
|
+
Requires-Dist: pyside6>=6.11.1
|
|
10
|
+
Requires-Dist: python-xlib>=0.33 ; sys_platform == 'linux'
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Project-URL: Homepage, https://github.com/codemee/prtools
|
|
13
|
+
Project-URL: Repository, https://github.com/codemee/prtools
|
|
14
|
+
Project-URL: Issues, https://github.com/codemee/prtools/issues
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# PRKit:簡報瑞士刀
|
|
18
|
+
|
|
19
|
+
[繁體中文](README.md) | [English](README.en.md)
|
|
20
|
+
|
|
21
|
+
跨平台的系統匣簡報輔助工具。程式不會開啟主視窗,所有設定都在系統匣圖示的右鍵選單中完成。
|
|
22
|
+
|
|
23
|
+
## 功能
|
|
24
|
+
|
|
25
|
+
- 聚光燈游標:可調整顏色、透明度與 48–320 px 圓圈大小;Windows 使用真正的系統游標,macOS/Linux 使用不攔截點擊的透明覆蓋層。
|
|
26
|
+
- 按鍵顯示:在游標所在螢幕中央下方顯示單鍵或組合鍵,不會攔截原本的鍵盤輸入。
|
|
27
|
+
- 自動保存:記住兩項功能的啟用狀態與外觀設定。
|
|
28
|
+
- 多螢幕與高 DPI:使用 Qt 邏輯像素定位與繪製。
|
|
29
|
+
|
|
30
|
+
## 從 PyPI 安裝
|
|
31
|
+
|
|
32
|
+
需要先安裝 [uv](https://docs.astral.sh/uv/),再執行:
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
uv tool install prkit
|
|
36
|
+
prkit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
也可以從已下載的專案目錄安裝:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
uv tool install .
|
|
43
|
+
prkit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
或直接從 GitHub 安裝最新正式版;`latest` 標籤會在每次正式發布時更新:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
uv tool install git+https://github.com/codemee/prtools.git@latest
|
|
50
|
+
prkit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.1.0`。
|
|
54
|
+
|
|
55
|
+
若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
|
|
56
|
+
|
|
57
|
+
## 使用 uvx 免安裝執行
|
|
58
|
+
|
|
59
|
+
直接從 PyPI 執行:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
uvx prkit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
也可以在已下載的專案目錄執行,或直接從 GitHub 執行最新正式版:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
uvx --from . prkit
|
|
69
|
+
uvx --from git+https://github.com/codemee/prtools.git@latest prkit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`uvx` 會在隔離環境中準備並執行程式,不會把 `prkit` 持久安裝到工具目錄。
|
|
73
|
+
|
|
74
|
+
## 開發
|
|
75
|
+
|
|
76
|
+
Python 3.12、虛擬環境及相依套件均由 uv 管理:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
uv sync
|
|
80
|
+
uv run prkit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
品質檢查:
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
uv run ruff check .
|
|
87
|
+
uv run ruff format --check .
|
|
88
|
+
uv run pytest -q
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 平台注意事項
|
|
92
|
+
|
|
93
|
+
- Windows:不需要額外權限。啟用聚光燈時會暫時替換系統游標,停用或結束時自動恢復;隨工具一併安裝的獨立 watchdog 會在主程式異常終止時復原游標。
|
|
94
|
+
- macOS:按鍵顯示使用 HID 層的唯讀事件監看,需要在「系統設定 → 隱私權與安全性 → 輸入監控」允許啟動本程式的應用程式(例如 ChatGPT、Terminal 或其他啟動 `prkit` 的終端程式)。監看器不會修改或攔截事件;即使組合鍵被其他快捷鍵工具處理,仍可先顯示按鍵。
|
|
95
|
+
- Linux:完整支援目標為 X11,桌面環境必須提供 StatusNotifierItem 或 XEmbed 系統匣。GNOME 可能需要 AppIndicator 類型的擴充套件。
|
|
96
|
+
- Wayland:受全域輸入與覆蓋層協定限制,按鍵顯示和點擊穿透不保證可用;程式會在選單中顯示警告。
|
|
97
|
+
|
|
98
|
+
按鍵內容只用於即時畫面顯示,不會寫入設定、檔案或日誌。
|
|
99
|
+
|
|
100
|
+
實作架構、macOS 原生視窗層級、權限模型與鍵盤事件流程請參閱[技術設計](docs/TECHNICAL.md)。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
prkit/__init__.py,sha256=S3gN5txBfmJ41RnyRwk6vSMj2etHuB4PMWYmzDjqJnk,69
|
|
2
|
+
prkit/__main__.py,sha256=Yxf43osXdzsSFHRiC5vtFsNRlJi3oGtCHEnffBJFdv8,53
|
|
3
|
+
prkit/app.py,sha256=w9rPbBPvT0LIM9deASDmzeRmdftkOMX_UreTZBO6GdI,6797
|
|
4
|
+
prkit/cursor_watchdog.py,sha256=emSwvjv6Rq_OipLY46ga0358_4wRifKdpsHAi_bUdYg,1369
|
|
5
|
+
prkit/icon.py,sha256=4JpRX_BgYywdK-PIJBkUrII8j68HhHJNlzzuyMlPp54,932
|
|
6
|
+
prkit/keyboard.py,sha256=TE_KFo3iUTq6zDPBTeUc6y1EOM7HRP8wKxMBcPGlg4s,2110
|
|
7
|
+
prkit/keys.py,sha256=AINvI4kek5PzQBBJx2Vko49vCraeQcXsIAHNKtrH6l8,3132
|
|
8
|
+
prkit/mac_keyboard.py,sha256=a2c4AntRos2rMq9bMlaDcCowR08rSsFzet8bCAy6k3o,6417
|
|
9
|
+
prkit/overlays.py,sha256=SZgN7igtgxtAUub2YVc4py6aBBJ_wFjS-IGHIK3sHao,6056
|
|
10
|
+
prkit/platform.py,sha256=NOoxBOfsNsFaloUe2M8-wqQpFxG2K9RN88AGm_Aal0Q,4809
|
|
11
|
+
prkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
prkit/settings.py,sha256=pZs3lg-vD6Sa57yW4gkMcJBrOxSgTk3xSQ7v73U8jr0,3187
|
|
13
|
+
prkit/spotlight.py,sha256=Oodu3viROiaqwFbM3WFIAoIGH4ymEcv2MB75ixZx91A,687
|
|
14
|
+
prkit/tray.py,sha256=JF368eMWEzOSh1iYttyfD_YlZwk33Kzx1Kb6e6bISJU,8685
|
|
15
|
+
prkit/windows_cursor.py,sha256=W_8dg6IhXYsDfH60gSPLWKNJZC_us-Dokxvnl3YhFa4,16118
|
|
16
|
+
prkit-0.1.0.dist-info/WHEEL,sha256=4OL6Foqnnp3xRY5wMkjgc25_i5YJC6dKsC6LPcjqEoU,80
|
|
17
|
+
prkit-0.1.0.dist-info/entry_points.txt,sha256=kp0Fsg2I36s8FJwhwL61MIUltXf9Y1z5355OByVYmhI,93
|
|
18
|
+
prkit-0.1.0.dist-info/METADATA,sha256=SAp2rYiKuX1BZp3UouP6pl5PoisYq0rG105rClrGVgo,3566
|
|
19
|
+
prkit-0.1.0.dist-info/RECORD,,
|