prkit 0.1.0__tar.gz
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-0.1.0/PKG-INFO +100 -0
- prkit-0.1.0/README.md +84 -0
- prkit-0.1.0/pyproject.toml +54 -0
- prkit-0.1.0/pyproject.toml.orig +46 -0
- prkit-0.1.0/src/prkit/__init__.py +3 -0
- prkit-0.1.0/src/prkit/__main__.py +3 -0
- prkit-0.1.0/src/prkit/app.py +183 -0
- prkit-0.1.0/src/prkit/cursor_watchdog.py +48 -0
- prkit-0.1.0/src/prkit/icon.py +25 -0
- prkit-0.1.0/src/prkit/keyboard.py +68 -0
- prkit-0.1.0/src/prkit/keys.py +111 -0
- prkit-0.1.0/src/prkit/mac_keyboard.py +240 -0
- prkit-0.1.0/src/prkit/overlays.py +170 -0
- prkit-0.1.0/src/prkit/platform.py +152 -0
- prkit-0.1.0/src/prkit/py.typed +0 -0
- prkit-0.1.0/src/prkit/settings.py +102 -0
- prkit-0.1.0/src/prkit/spotlight.py +26 -0
- prkit-0.1.0/src/prkit/tray.py +242 -0
- prkit-0.1.0/src/prkit/windows_cursor.py +467 -0
prkit-0.1.0/PKG-INFO
ADDED
|
@@ -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)。
|
prkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# PRKit:簡報瑞士刀
|
|
2
|
+
|
|
3
|
+
[繁體中文](README.md) | [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
跨平台的系統匣簡報輔助工具。程式不會開啟主視窗,所有設定都在系統匣圖示的右鍵選單中完成。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- 聚光燈游標:可調整顏色、透明度與 48–320 px 圓圈大小;Windows 使用真正的系統游標,macOS/Linux 使用不攔截點擊的透明覆蓋層。
|
|
10
|
+
- 按鍵顯示:在游標所在螢幕中央下方顯示單鍵或組合鍵,不會攔截原本的鍵盤輸入。
|
|
11
|
+
- 自動保存:記住兩項功能的啟用狀態與外觀設定。
|
|
12
|
+
- 多螢幕與高 DPI:使用 Qt 邏輯像素定位與繪製。
|
|
13
|
+
|
|
14
|
+
## 從 PyPI 安裝
|
|
15
|
+
|
|
16
|
+
需要先安裝 [uv](https://docs.astral.sh/uv/),再執行:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
uv tool install prkit
|
|
20
|
+
prkit
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
也可以從已下載的專案目錄安裝:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
uv tool install .
|
|
27
|
+
prkit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
或直接從 GitHub 安裝最新正式版;`latest` 標籤會在每次正式發布時更新:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
uv tool install git+https://github.com/codemee/prtools.git@latest
|
|
34
|
+
prkit
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.1.0`。
|
|
38
|
+
|
|
39
|
+
若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
|
|
40
|
+
|
|
41
|
+
## 使用 uvx 免安裝執行
|
|
42
|
+
|
|
43
|
+
直接從 PyPI 執行:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
uvx prkit
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
也可以在已下載的專案目錄執行,或直接從 GitHub 執行最新正式版:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
uvx --from . prkit
|
|
53
|
+
uvx --from git+https://github.com/codemee/prtools.git@latest prkit
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`uvx` 會在隔離環境中準備並執行程式,不會把 `prkit` 持久安裝到工具目錄。
|
|
57
|
+
|
|
58
|
+
## 開發
|
|
59
|
+
|
|
60
|
+
Python 3.12、虛擬環境及相依套件均由 uv 管理:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
uv sync
|
|
64
|
+
uv run prkit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
品質檢查:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
uv run ruff check .
|
|
71
|
+
uv run ruff format --check .
|
|
72
|
+
uv run pytest -q
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 平台注意事項
|
|
76
|
+
|
|
77
|
+
- Windows:不需要額外權限。啟用聚光燈時會暫時替換系統游標,停用或結束時自動恢復;隨工具一併安裝的獨立 watchdog 會在主程式異常終止時復原游標。
|
|
78
|
+
- macOS:按鍵顯示使用 HID 層的唯讀事件監看,需要在「系統設定 → 隱私權與安全性 → 輸入監控」允許啟動本程式的應用程式(例如 ChatGPT、Terminal 或其他啟動 `prkit` 的終端程式)。監看器不會修改或攔截事件;即使組合鍵被其他快捷鍵工具處理,仍可先顯示按鍵。
|
|
79
|
+
- Linux:完整支援目標為 X11,桌面環境必須提供 StatusNotifierItem 或 XEmbed 系統匣。GNOME 可能需要 AppIndicator 類型的擴充套件。
|
|
80
|
+
- Wayland:受全域輸入與覆蓋層協定限制,按鍵顯示和點擊穿透不保證可用;程式會在選單中顯示警告。
|
|
81
|
+
|
|
82
|
+
按鍵內容只用於即時畫面顯示,不會寫入設定、檔案或日誌。
|
|
83
|
+
|
|
84
|
+
實作架構、macOS 原生視窗層級、權限模型與鍵盤事件流程請參閱[技術設計](docs/TECHNICAL.md)。
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "prkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A cross-platform system-tray presentation toolkit"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pynput>=1.8.2",
|
|
9
|
+
"pyobjc-framework-cocoa>=12.2.1 ; sys_platform == 'darwin'",
|
|
10
|
+
"pyside6>=6.11.1",
|
|
11
|
+
"python-xlib>=0.33 ; sys_platform == 'linux'",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "meebox"
|
|
16
|
+
email = "meebox@gmail.com"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
prkit = "prkit.app:main"
|
|
20
|
+
prkit-cursor-watchdog = "prkit.cursor_watchdog:main"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/codemee/prtools"
|
|
24
|
+
Repository = "https://github.com/codemee/prtools"
|
|
25
|
+
Issues = "https://github.com/codemee/prtools/issues"
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
addopts = "-q"
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 100
|
|
33
|
+
target-version = "py312"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = [
|
|
37
|
+
"E",
|
|
38
|
+
"F",
|
|
39
|
+
"I",
|
|
40
|
+
"UP",
|
|
41
|
+
"B",
|
|
42
|
+
"SIM",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
47
|
+
build-backend = "uv_build"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=9.1.1",
|
|
52
|
+
"pytest-qt>=4.5.0",
|
|
53
|
+
"ruff>=0.15.21",
|
|
54
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "prkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A cross-platform system-tray presentation toolkit"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "meebox", email = "meebox@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pynput>=1.8.2",
|
|
12
|
+
"pyobjc-framework-cocoa>=12.2.1 ; sys_platform == 'darwin'",
|
|
13
|
+
"pyside6>=6.11.1",
|
|
14
|
+
"python-xlib>=0.33 ; sys_platform == 'linux'",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
prkit = "prkit.app:main"
|
|
19
|
+
prkit-cursor-watchdog = "prkit.cursor_watchdog:main"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/codemee/prtools"
|
|
23
|
+
Repository = "https://github.com/codemee/prtools"
|
|
24
|
+
Issues = "https://github.com/codemee/prtools/issues"
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
addopts = "-q"
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
line-length = 100
|
|
32
|
+
target-version = "py312"
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
39
|
+
build-backend = "uv_build"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=9.1.1",
|
|
44
|
+
"pytest-qt>=4.5.0",
|
|
45
|
+
"ruff>=0.15.21",
|
|
46
|
+
]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QObject, QSignalBlocker, QTimer
|
|
8
|
+
from PySide6.QtWidgets import QApplication, QMessageBox
|
|
9
|
+
|
|
10
|
+
from prkit.keyboard import KeyboardMonitor
|
|
11
|
+
from prkit.overlays import KeystrokeOverlay
|
|
12
|
+
from prkit.settings import SettingsStore, SettingsStoreProtocol
|
|
13
|
+
from prkit.spotlight import SpotlightBackend, create_spotlight
|
|
14
|
+
from prkit.tray import TrayController
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AppController(QObject):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
app: QApplication,
|
|
21
|
+
store: SettingsStoreProtocol | None = None,
|
|
22
|
+
monitor: KeyboardMonitor | None = None,
|
|
23
|
+
spotlight: SpotlightBackend | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._app = app
|
|
27
|
+
self._store = store or SettingsStore()
|
|
28
|
+
self.settings = self._store.load()
|
|
29
|
+
self.spotlight = spotlight or create_spotlight(self.settings.spotlight)
|
|
30
|
+
self.keystroke = KeystrokeOverlay(self.settings.keystroke)
|
|
31
|
+
self.monitor = monitor or KeyboardMonitor(self)
|
|
32
|
+
self.tray = TrayController(self.settings, self)
|
|
33
|
+
self._shutting_down = False
|
|
34
|
+
self._keystroke_start_pending = False
|
|
35
|
+
|
|
36
|
+
self.tray.connect_settings(
|
|
37
|
+
spotlight_enabled=self.set_spotlight_enabled,
|
|
38
|
+
spotlight_color=self.set_spotlight_color,
|
|
39
|
+
spotlight_opacity=self.set_spotlight_opacity,
|
|
40
|
+
spotlight_size=self.set_spotlight_size,
|
|
41
|
+
keystroke_enabled=self.set_keystroke_enabled,
|
|
42
|
+
keystroke_color=self.set_keystroke_color,
|
|
43
|
+
keystroke_opacity=self.set_keystroke_opacity,
|
|
44
|
+
)
|
|
45
|
+
self.tray.exit_requested.connect(self.shutdown)
|
|
46
|
+
self.tray.menu.aboutToShow.connect(self.spotlight.sync_position)
|
|
47
|
+
self.tray.menu.aboutToHide.connect(self._start_pending_keyboard_monitor)
|
|
48
|
+
self.monitor.chord_pressed.connect(self.keystroke.show_chord)
|
|
49
|
+
self.monitor.all_released.connect(self.keystroke.keys_released)
|
|
50
|
+
self.monitor.failed.connect(self._keyboard_failed)
|
|
51
|
+
self._app.aboutToQuit.connect(self.cleanup)
|
|
52
|
+
|
|
53
|
+
def start(self) -> None:
|
|
54
|
+
self.tray.show()
|
|
55
|
+
self.spotlight.set_enabled(self.settings.spotlight.enabled)
|
|
56
|
+
if self.settings.keystroke.enabled:
|
|
57
|
+
self.set_keystroke_enabled(True)
|
|
58
|
+
|
|
59
|
+
def set_spotlight_enabled(self, enabled: bool) -> None:
|
|
60
|
+
try:
|
|
61
|
+
self.spotlight.set_enabled(enabled)
|
|
62
|
+
except Exception as error:
|
|
63
|
+
self._disable_spotlight_ui()
|
|
64
|
+
self.tray.notify_warning(f"無法啟用聚光燈游標:{error}")
|
|
65
|
+
return
|
|
66
|
+
self.settings.spotlight.enabled = enabled
|
|
67
|
+
self._save()
|
|
68
|
+
|
|
69
|
+
def set_spotlight_color(self, color: str) -> None:
|
|
70
|
+
self.settings.spotlight.color = color
|
|
71
|
+
self._apply_spotlight_appearance()
|
|
72
|
+
|
|
73
|
+
def set_spotlight_opacity(self, opacity: int) -> None:
|
|
74
|
+
self.settings.spotlight.opacity = opacity
|
|
75
|
+
self._apply_spotlight_appearance()
|
|
76
|
+
|
|
77
|
+
def set_spotlight_size(self, size: int) -> None:
|
|
78
|
+
self.settings.spotlight.size = size
|
|
79
|
+
self._apply_spotlight_appearance()
|
|
80
|
+
|
|
81
|
+
def _apply_spotlight_appearance(self) -> None:
|
|
82
|
+
value = self.settings.spotlight
|
|
83
|
+
try:
|
|
84
|
+
self.spotlight.set_appearance(value.color, value.opacity, value.size)
|
|
85
|
+
except Exception as error:
|
|
86
|
+
self._disable_spotlight_ui()
|
|
87
|
+
self.tray.notify_warning(f"無法更新聚光燈游標:{error}")
|
|
88
|
+
return
|
|
89
|
+
self._save()
|
|
90
|
+
|
|
91
|
+
def _disable_spotlight_ui(self) -> None:
|
|
92
|
+
with suppress(Exception):
|
|
93
|
+
self.spotlight.set_enabled(False)
|
|
94
|
+
self.settings.spotlight.enabled = False
|
|
95
|
+
with QSignalBlocker(self.tray.panel.spotlight_enabled):
|
|
96
|
+
self.tray.panel.set_spotlight_enabled(False)
|
|
97
|
+
self._save()
|
|
98
|
+
|
|
99
|
+
def set_keystroke_enabled(self, enabled: bool) -> None:
|
|
100
|
+
if enabled:
|
|
101
|
+
self._keystroke_start_pending = True
|
|
102
|
+
if not self.tray.menu.isVisible():
|
|
103
|
+
QTimer.singleShot(0, self._start_pending_keyboard_monitor)
|
|
104
|
+
return
|
|
105
|
+
else:
|
|
106
|
+
self._keystroke_start_pending = False
|
|
107
|
+
self.monitor.stop()
|
|
108
|
+
self.keystroke.hide_now()
|
|
109
|
+
self.settings.keystroke.enabled = enabled
|
|
110
|
+
self._save()
|
|
111
|
+
|
|
112
|
+
def _start_pending_keyboard_monitor(self) -> None:
|
|
113
|
+
if not self._keystroke_start_pending:
|
|
114
|
+
return
|
|
115
|
+
self._keystroke_start_pending = False
|
|
116
|
+
success, error = self.monitor.start()
|
|
117
|
+
if not success:
|
|
118
|
+
self._disable_keystroke_ui()
|
|
119
|
+
self.tray.notify_warning(f"無法啟用按鍵顯示:{error or '未知錯誤'}")
|
|
120
|
+
return
|
|
121
|
+
self.settings.keystroke.enabled = True
|
|
122
|
+
self._save()
|
|
123
|
+
|
|
124
|
+
def set_keystroke_color(self, color: str) -> None:
|
|
125
|
+
self.settings.keystroke.color = color
|
|
126
|
+
self._apply_keystroke_appearance()
|
|
127
|
+
|
|
128
|
+
def set_keystroke_opacity(self, opacity: int) -> None:
|
|
129
|
+
self.settings.keystroke.opacity = opacity
|
|
130
|
+
self._apply_keystroke_appearance()
|
|
131
|
+
|
|
132
|
+
def _apply_keystroke_appearance(self) -> None:
|
|
133
|
+
value = self.settings.keystroke
|
|
134
|
+
self.keystroke.set_appearance(value.color, value.opacity)
|
|
135
|
+
self._save()
|
|
136
|
+
|
|
137
|
+
def _keyboard_failed(self, message: str) -> None:
|
|
138
|
+
self._disable_keystroke_ui()
|
|
139
|
+
self.tray.notify_warning(f"按鍵監聽已停止:{message}")
|
|
140
|
+
|
|
141
|
+
def _disable_keystroke_ui(self) -> None:
|
|
142
|
+
self._keystroke_start_pending = False
|
|
143
|
+
self.monitor.stop()
|
|
144
|
+
self.keystroke.hide_now()
|
|
145
|
+
self.settings.keystroke.enabled = False
|
|
146
|
+
with QSignalBlocker(self.tray.panel.keystroke_enabled):
|
|
147
|
+
self.tray.panel.set_keystroke_enabled(False)
|
|
148
|
+
self._save()
|
|
149
|
+
|
|
150
|
+
def _save(self) -> None:
|
|
151
|
+
self._store.save(self.settings)
|
|
152
|
+
|
|
153
|
+
def shutdown(self) -> None:
|
|
154
|
+
if self._shutting_down:
|
|
155
|
+
return
|
|
156
|
+
self._shutting_down = True
|
|
157
|
+
self.cleanup()
|
|
158
|
+
self._app.quit()
|
|
159
|
+
|
|
160
|
+
def cleanup(self) -> None:
|
|
161
|
+
self._keystroke_start_pending = False
|
|
162
|
+
self.monitor.stop()
|
|
163
|
+
self.spotlight.set_enabled(False)
|
|
164
|
+
self.keystroke.hide_now()
|
|
165
|
+
self.tray.hide()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def create_application(arguments: Sequence[str] | None = None) -> QApplication:
|
|
169
|
+
app = QApplication(list(arguments) if arguments is not None else sys.argv)
|
|
170
|
+
app.setApplicationName("簡報瑞士刀")
|
|
171
|
+
app.setOrganizationName("prkit")
|
|
172
|
+
app.setQuitOnLastWindowClosed(False)
|
|
173
|
+
return app
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main() -> int:
|
|
177
|
+
app = create_application()
|
|
178
|
+
if not TrayController.is_available():
|
|
179
|
+
QMessageBox.critical(None, "簡報瑞士刀", "此桌面環境沒有可用的系統匣。")
|
|
180
|
+
return 1
|
|
181
|
+
controller = AppController(app)
|
|
182
|
+
controller.start()
|
|
183
|
+
return app.exec()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import sys
|
|
5
|
+
from ctypes import wintypes
|
|
6
|
+
|
|
7
|
+
SPI_SETCURSORS = 0x0057
|
|
8
|
+
SYNCHRONIZE = 0x00100000
|
|
9
|
+
WAIT_OBJECT_0 = 0
|
|
10
|
+
INFINITE = 0xFFFFFFFF
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def restore_system_cursors() -> None:
|
|
14
|
+
ctypes.windll.user32.SystemParametersInfoW(SPI_SETCURSORS, 0, None, 0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def watch(parent_pid: int, stop_event_name: str) -> int:
|
|
18
|
+
kernel32 = ctypes.windll.kernel32
|
|
19
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
20
|
+
kernel32.OpenEventW.restype = wintypes.HANDLE
|
|
21
|
+
parent = kernel32.OpenProcess(SYNCHRONIZE, False, parent_pid)
|
|
22
|
+
stop_event = kernel32.OpenEventW(SYNCHRONIZE, False, stop_event_name)
|
|
23
|
+
if not parent or not stop_event:
|
|
24
|
+
restore_system_cursors()
|
|
25
|
+
if parent:
|
|
26
|
+
kernel32.CloseHandle(parent)
|
|
27
|
+
if stop_event:
|
|
28
|
+
kernel32.CloseHandle(stop_event)
|
|
29
|
+
return 1
|
|
30
|
+
try:
|
|
31
|
+
handles = (wintypes.HANDLE * 2)(parent, stop_event)
|
|
32
|
+
result = kernel32.WaitForMultipleObjects(2, handles, False, INFINITE)
|
|
33
|
+
if result == WAIT_OBJECT_0:
|
|
34
|
+
restore_system_cursors()
|
|
35
|
+
finally:
|
|
36
|
+
kernel32.CloseHandle(parent)
|
|
37
|
+
kernel32.CloseHandle(stop_event)
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> int:
|
|
42
|
+
if sys.platform != "win32" or len(sys.argv) != 3:
|
|
43
|
+
return 2
|
|
44
|
+
return watch(int(sys.argv[1]), sys.argv[2])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import QByteArray
|
|
4
|
+
from PySide6.QtGui import QIcon, QPainter, QPixmap
|
|
5
|
+
from PySide6.QtSvg import QSvgRenderer
|
|
6
|
+
|
|
7
|
+
TRAY_ICON_SVG = b"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
8
|
+
<defs><radialGradient id="g"><stop offset="0" stop-color="#fff"/>
|
|
9
|
+
<stop offset=".18" stop-color="#ffb3b3"/><stop offset=".48" stop-color="#f44336"/>
|
|
10
|
+
<stop offset="1" stop-color="#8b0000" stop-opacity=".25"/></radialGradient></defs>
|
|
11
|
+
<circle cx="32" cy="32" r="27" fill="url(#g)"/><circle cx="32" cy="32" r="8" fill="#fff"/>
|
|
12
|
+
</svg>"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def tray_icon() -> QIcon:
|
|
16
|
+
renderer = QSvgRenderer(QByteArray(TRAY_ICON_SVG))
|
|
17
|
+
icon = QIcon()
|
|
18
|
+
for size in (16, 20, 24, 32, 48, 64):
|
|
19
|
+
pixmap = QPixmap(size, size)
|
|
20
|
+
pixmap.fill("transparent")
|
|
21
|
+
painter = QPainter(pixmap)
|
|
22
|
+
renderer.render(painter)
|
|
23
|
+
painter.end()
|
|
24
|
+
icon.addPixmap(pixmap)
|
|
25
|
+
return icon
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import QObject, Signal
|
|
7
|
+
|
|
8
|
+
from prkit.keys import KeyChordTracker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KeyboardMonitor(QObject):
|
|
12
|
+
chord_pressed = Signal(str)
|
|
13
|
+
all_released = Signal()
|
|
14
|
+
failed = Signal(str)
|
|
15
|
+
|
|
16
|
+
def __init__(self, parent: QObject | None = None) -> None:
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
self._listener: Any | None = None
|
|
19
|
+
self._tracker = KeyChordTracker()
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def running(self) -> bool:
|
|
23
|
+
return self._listener is not None
|
|
24
|
+
|
|
25
|
+
def start(self) -> tuple[bool, str | None]:
|
|
26
|
+
if self._listener is not None:
|
|
27
|
+
return True, None
|
|
28
|
+
try:
|
|
29
|
+
if sys.platform == "darwin":
|
|
30
|
+
from prkit.mac_keyboard import MacKeyboardListener
|
|
31
|
+
|
|
32
|
+
listener = MacKeyboardListener(self._on_press, self._on_release)
|
|
33
|
+
success, error = listener.start()
|
|
34
|
+
if not success:
|
|
35
|
+
return False, error
|
|
36
|
+
else:
|
|
37
|
+
from pynput import keyboard
|
|
38
|
+
|
|
39
|
+
listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release)
|
|
40
|
+
listener.start()
|
|
41
|
+
listener.wait()
|
|
42
|
+
self._listener = listener
|
|
43
|
+
except Exception as error: # pynput exposes backend-specific exceptions
|
|
44
|
+
self._listener = None
|
|
45
|
+
self._tracker.clear()
|
|
46
|
+
return False, str(error)
|
|
47
|
+
return True, None
|
|
48
|
+
|
|
49
|
+
def stop(self) -> None:
|
|
50
|
+
listener, self._listener = self._listener, None
|
|
51
|
+
self._tracker.clear()
|
|
52
|
+
if listener is not None:
|
|
53
|
+
listener.stop()
|
|
54
|
+
|
|
55
|
+
def _on_press(self, key: object) -> None:
|
|
56
|
+
try:
|
|
57
|
+
label = self._tracker.press(key)
|
|
58
|
+
if label:
|
|
59
|
+
self.chord_pressed.emit(label)
|
|
60
|
+
except Exception as error:
|
|
61
|
+
self.failed.emit(str(error))
|
|
62
|
+
|
|
63
|
+
def _on_release(self, key: object) -> None:
|
|
64
|
+
try:
|
|
65
|
+
if self._tracker.release(key):
|
|
66
|
+
self.all_released.emit()
|
|
67
|
+
except Exception as error:
|
|
68
|
+
self.failed.emit(str(error))
|