prkit 0.2.1__tar.gz → 0.3.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.2.1 → prkit-0.3.0}/PKG-INFO +7 -5
- {prkit-0.2.1 → prkit-0.3.0}/README.md +6 -4
- {prkit-0.2.1 → prkit-0.3.0}/pyproject.toml +1 -1
- {prkit-0.2.1 → prkit-0.3.0}/pyproject.toml.orig +1 -1
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/__init__.py +1 -1
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/app.py +47 -8
- prkit-0.3.0/src/prkit/autostart.py +90 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/icon.py +22 -1
- prkit-0.3.0/src/prkit/language.py +53 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/settings.py +6 -1
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/tray.py +83 -7
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/__main__.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/capslock.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/cursor_watchdog.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/input_source.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/keyboard.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/keys.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/mac_capslock.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/mac_keyboard.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/overlays.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/platform.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/py.typed +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/spotlight.py +0 -0
- {prkit-0.2.1 → prkit-0.3.0}/src/prkit/windows_cursor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: prkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A cross-platform system-tray presentation toolkit
|
|
5
5
|
Author: meebox
|
|
6
6
|
Author-email: meebox <meebox@gmail.com>
|
|
@@ -36,14 +36,16 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
|
|
37
37
|
| 平台 | 下載檔案 | 啟動方式 |
|
|
38
38
|
| --- | --- | --- |
|
|
39
|
-
| Windows x64 | `PRKit-0.
|
|
40
|
-
| macOS Apple Silicon | `PRKit-0.
|
|
39
|
+
| Windows x64 | `PRKit-0.3.0-windows-x64.exe` | 直接開啟執行 |
|
|
40
|
+
| macOS Apple Silicon | `PRKit-0.3.0-macos-arm64.dmg` | 開啟 DMG,將 `PRKit.app` 拖到「Applications」捷徑後啟動 |
|
|
41
41
|
|
|
42
42
|
Windows 為單一 EXE,已包含執行環境與游標復原 watchdog,不需額外下載檔案。macOS 目前僅提供 Apple Silicon 版。程式啟動後顯示在系統匣/選單列,不會開啟主視窗。
|
|
43
43
|
|
|
44
|
+
系統匣選單中的「開機自動啟動」會在目前使用者登入時啟動程式,取消勾選即可移除。語言按鈕依序切換地球(隨系統)、「中」(中文)和「En」(英文);切換會立即生效並保存。
|
|
45
|
+
|
|
44
46
|
桌面版尚未使用 Windows 發行者憑證或 Apple Developer ID 簽署/公證;macOS 僅使用 ad-hoc 簽章。若系統阻擋你確認來源的下載檔案,請依系統安全提示允許開啟。macOS 按鍵顯示仍需在「輸入監控」允許 `PRKit`;輸入法狀態模式不需此權限。每個下載檔另提供 `.sha256` 校驗檔。
|
|
45
47
|
|
|
46
|
-
版本標籤會自動觸發 GitHub
|
|
48
|
+
版本標籤會自動觸發 GitHub 雲端桌面打包、成品測試與 PyPI 發佈;手動桌面建置可在 Actions 的 **Build desktop apps** 執行,成品在該次執行的 Artifacts。詳細操作見[發佈指南](docs/RELEASING.md)。
|
|
47
49
|
|
|
48
50
|
## 從 PyPI 安裝
|
|
49
51
|
|
|
@@ -68,7 +70,7 @@ uv tool install git+https://github.com/codemee/prkit.git@latest
|
|
|
68
70
|
prkit
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.
|
|
73
|
+
若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.3.0`。
|
|
72
74
|
|
|
73
75
|
若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
|
|
74
76
|
|
|
@@ -19,14 +19,16 @@
|
|
|
19
19
|
|
|
20
20
|
| 平台 | 下載檔案 | 啟動方式 |
|
|
21
21
|
| --- | --- | --- |
|
|
22
|
-
| Windows x64 | `PRKit-0.
|
|
23
|
-
| macOS Apple Silicon | `PRKit-0.
|
|
22
|
+
| Windows x64 | `PRKit-0.3.0-windows-x64.exe` | 直接開啟執行 |
|
|
23
|
+
| macOS Apple Silicon | `PRKit-0.3.0-macos-arm64.dmg` | 開啟 DMG,將 `PRKit.app` 拖到「Applications」捷徑後啟動 |
|
|
24
24
|
|
|
25
25
|
Windows 為單一 EXE,已包含執行環境與游標復原 watchdog,不需額外下載檔案。macOS 目前僅提供 Apple Silicon 版。程式啟動後顯示在系統匣/選單列,不會開啟主視窗。
|
|
26
26
|
|
|
27
|
+
系統匣選單中的「開機自動啟動」會在目前使用者登入時啟動程式,取消勾選即可移除。語言按鈕依序切換地球(隨系統)、「中」(中文)和「En」(英文);切換會立即生效並保存。
|
|
28
|
+
|
|
27
29
|
桌面版尚未使用 Windows 發行者憑證或 Apple Developer ID 簽署/公證;macOS 僅使用 ad-hoc 簽章。若系統阻擋你確認來源的下載檔案,請依系統安全提示允許開啟。macOS 按鍵顯示仍需在「輸入監控」允許 `PRKit`;輸入法狀態模式不需此權限。每個下載檔另提供 `.sha256` 校驗檔。
|
|
28
30
|
|
|
29
|
-
版本標籤會自動觸發 GitHub
|
|
31
|
+
版本標籤會自動觸發 GitHub 雲端桌面打包、成品測試與 PyPI 發佈;手動桌面建置可在 Actions 的 **Build desktop apps** 執行,成品在該次執行的 Artifacts。詳細操作見[發佈指南](docs/RELEASING.md)。
|
|
30
32
|
|
|
31
33
|
## 從 PyPI 安裝
|
|
32
34
|
|
|
@@ -51,7 +53,7 @@ uv tool install git+https://github.com/codemee/prkit.git@latest
|
|
|
51
53
|
prkit
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.
|
|
56
|
+
若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.3.0`。
|
|
55
57
|
|
|
56
58
|
若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
|
|
57
59
|
|
|
@@ -7,9 +7,11 @@ from contextlib import suppress
|
|
|
7
7
|
from PySide6.QtCore import QObject, QSignalBlocker, QTimer
|
|
8
8
|
from PySide6.QtWidgets import QApplication, QMessageBox
|
|
9
9
|
|
|
10
|
+
from prkit import autostart
|
|
10
11
|
from prkit.capslock import CapsLockMonitor
|
|
11
12
|
from prkit.input_source import InputSourceMonitor
|
|
12
13
|
from prkit.keyboard import KeyboardMonitor
|
|
14
|
+
from prkit.language import effective_language, tr
|
|
13
15
|
from prkit.overlays import CapsLockOverlay, KeystrokeOverlay
|
|
14
16
|
from prkit.settings import SettingsStore, SettingsStoreProtocol
|
|
15
17
|
from prkit.spotlight import SpotlightBackend, create_spotlight
|
|
@@ -33,6 +35,9 @@ class AppController(QObject):
|
|
|
33
35
|
self.spotlight = spotlight or create_spotlight(self.settings.spotlight)
|
|
34
36
|
self.keystroke = KeystrokeOverlay(self.settings.keystroke)
|
|
35
37
|
self.capslock = CapsLockOverlay(self.settings.capslock)
|
|
38
|
+
self.capslock.setToolTip(
|
|
39
|
+
tr("拖曳以移動狀態提示", effective_language(self.settings.language))
|
|
40
|
+
)
|
|
36
41
|
self.capslock_monitor = capslock_monitor or CapsLockMonitor(self)
|
|
37
42
|
self.input_source_monitor = input_source_monitor or InputSourceMonitor(self)
|
|
38
43
|
if sys.platform != "darwin":
|
|
@@ -52,6 +57,9 @@ class AppController(QObject):
|
|
|
52
57
|
keystroke_opacity=self.set_keystroke_opacity,
|
|
53
58
|
)
|
|
54
59
|
self.tray.exit_requested.connect(self.shutdown)
|
|
60
|
+
self.tray.autostart_changed.connect(self.set_autostart_enabled)
|
|
61
|
+
self.tray.language_changed.connect(self.set_language)
|
|
62
|
+
self.tray.menu.aboutToShow.connect(self.refresh_autostart)
|
|
55
63
|
self.tray.panel.capslock_enabled_changed.connect(self.set_capslock_enabled)
|
|
56
64
|
self.tray.panel.capslock_color_changed.connect(self.set_capslock_color)
|
|
57
65
|
self.tray.panel.capslock_opacity_changed.connect(self.set_capslock_opacity)
|
|
@@ -70,6 +78,7 @@ class AppController(QObject):
|
|
|
70
78
|
self._app.aboutToQuit.connect(self.cleanup)
|
|
71
79
|
|
|
72
80
|
def start(self) -> None:
|
|
81
|
+
self.refresh_autostart()
|
|
73
82
|
self.tray.show()
|
|
74
83
|
self.spotlight.set_enabled(self.settings.spotlight.enabled)
|
|
75
84
|
if self.settings.keystroke.enabled:
|
|
@@ -77,6 +86,31 @@ class AppController(QObject):
|
|
|
77
86
|
if self.settings.capslock.enabled:
|
|
78
87
|
self.set_capslock_enabled(True)
|
|
79
88
|
|
|
89
|
+
def refresh_autostart(self) -> None:
|
|
90
|
+
try:
|
|
91
|
+
self.tray.set_autostart_checked(autostart.is_enabled())
|
|
92
|
+
except OSError as error:
|
|
93
|
+
self.tray.notify_warning(self._text("無法設定開機自動啟動:{error}", error=error))
|
|
94
|
+
|
|
95
|
+
def set_autostart_enabled(self, enabled: bool) -> None:
|
|
96
|
+
try:
|
|
97
|
+
autostart.set_enabled(enabled)
|
|
98
|
+
except OSError as error:
|
|
99
|
+
self.tray.set_autostart_checked(not enabled)
|
|
100
|
+
self.refresh_autostart()
|
|
101
|
+
self.tray.notify_warning(self._text("無法設定開機自動啟動:{error}", error=error))
|
|
102
|
+
|
|
103
|
+
def set_language(self, preference: str) -> None:
|
|
104
|
+
if preference not in {"system", "zh", "en"}:
|
|
105
|
+
return
|
|
106
|
+
self.settings.language = preference
|
|
107
|
+
self.tray.set_language(preference)
|
|
108
|
+
self.capslock.setToolTip(self._text("拖曳以移動狀態提示"))
|
|
109
|
+
self._save()
|
|
110
|
+
|
|
111
|
+
def _text(self, source: str, **values: object) -> str:
|
|
112
|
+
return tr(source, effective_language(self.settings.language)).format(**values)
|
|
113
|
+
|
|
80
114
|
def set_capslock_enabled(self, enabled: bool) -> None:
|
|
81
115
|
self.settings.capslock.enabled = enabled
|
|
82
116
|
self.capslock_monitor.stop()
|
|
@@ -106,7 +140,7 @@ class AppController(QObject):
|
|
|
106
140
|
|
|
107
141
|
def _show_input_source(self, name: str) -> None:
|
|
108
142
|
if self.settings.capslock.mode == "input_source":
|
|
109
|
-
self.capslock.show_input_source(name)
|
|
143
|
+
self.capslock.show_input_source(self._text(name))
|
|
110
144
|
|
|
111
145
|
def set_capslock_color(self, color: str) -> None:
|
|
112
146
|
self.settings.capslock.color = color
|
|
@@ -132,14 +166,14 @@ class AppController(QObject):
|
|
|
132
166
|
self.set_capslock_enabled(False)
|
|
133
167
|
with QSignalBlocker(self.tray.panel.capslock_enabled):
|
|
134
168
|
self.tray.panel.capslock_enabled.setChecked(False)
|
|
135
|
-
self.tray.notify_warning(
|
|
169
|
+
self.tray.notify_warning(self._text("無法顯示鍵盤狀態:{message}", message=message))
|
|
136
170
|
|
|
137
171
|
def set_spotlight_enabled(self, enabled: bool) -> None:
|
|
138
172
|
try:
|
|
139
173
|
self.spotlight.set_enabled(enabled)
|
|
140
174
|
except Exception as error:
|
|
141
175
|
self._disable_spotlight_ui()
|
|
142
|
-
self.tray.notify_warning(
|
|
176
|
+
self.tray.notify_warning(self._text("無法啟用聚光燈游標:{error}", error=error))
|
|
143
177
|
return
|
|
144
178
|
self.settings.spotlight.enabled = enabled
|
|
145
179
|
self._save()
|
|
@@ -162,7 +196,7 @@ class AppController(QObject):
|
|
|
162
196
|
self.spotlight.set_appearance(value.color, value.opacity, value.size)
|
|
163
197
|
except Exception as error:
|
|
164
198
|
self._disable_spotlight_ui()
|
|
165
|
-
self.tray.notify_warning(
|
|
199
|
+
self.tray.notify_warning(self._text("無法更新聚光燈游標:{error}", error=error))
|
|
166
200
|
return
|
|
167
201
|
self._save()
|
|
168
202
|
|
|
@@ -194,7 +228,9 @@ class AppController(QObject):
|
|
|
194
228
|
success, error = self.monitor.start()
|
|
195
229
|
if not success:
|
|
196
230
|
self._disable_keystroke_ui()
|
|
197
|
-
self.tray.notify_warning(
|
|
231
|
+
self.tray.notify_warning(
|
|
232
|
+
self._text("無法啟用按鍵顯示:{error}", error=error or self._text("未知錯誤"))
|
|
233
|
+
)
|
|
198
234
|
return
|
|
199
235
|
self.settings.keystroke.enabled = True
|
|
200
236
|
self._save()
|
|
@@ -214,7 +250,7 @@ class AppController(QObject):
|
|
|
214
250
|
|
|
215
251
|
def _keyboard_failed(self, message: str) -> None:
|
|
216
252
|
self._disable_keystroke_ui()
|
|
217
|
-
self.tray.notify_warning(
|
|
253
|
+
self.tray.notify_warning(self._text("按鍵監聽已停止:{message}", message=message))
|
|
218
254
|
|
|
219
255
|
def _disable_keystroke_ui(self) -> None:
|
|
220
256
|
self._keystroke_start_pending = False
|
|
@@ -248,8 +284,8 @@ class AppController(QObject):
|
|
|
248
284
|
|
|
249
285
|
def create_application(arguments: Sequence[str] | None = None) -> QApplication:
|
|
250
286
|
app = QApplication(list(arguments) if arguments is not None else sys.argv)
|
|
251
|
-
app.setApplicationName("簡報瑞士刀")
|
|
252
287
|
app.setOrganizationName("prkit")
|
|
288
|
+
app.setApplicationName("簡報瑞士刀")
|
|
253
289
|
app.setQuitOnLastWindowClosed(False)
|
|
254
290
|
return app
|
|
255
291
|
|
|
@@ -257,7 +293,10 @@ def create_application(arguments: Sequence[str] | None = None) -> QApplication:
|
|
|
257
293
|
def main() -> int:
|
|
258
294
|
app = create_application()
|
|
259
295
|
if not TrayController.is_available():
|
|
260
|
-
|
|
296
|
+
language = effective_language(SettingsStore().load().language)
|
|
297
|
+
QMessageBox.critical(
|
|
298
|
+
None, tr("簡報瑞士刀", language), tr("此桌面環境沒有可用的系統匣。", language)
|
|
299
|
+
)
|
|
261
300
|
return 1
|
|
262
301
|
controller = AppController(app)
|
|
263
302
|
controller.start()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Manage per-user login startup without changing application settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import plistlib
|
|
7
|
+
import sys
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def launch_command() -> list[str]:
|
|
13
|
+
executable = Path(sys.executable)
|
|
14
|
+
if getattr(sys, "frozen", False):
|
|
15
|
+
return [str(executable)]
|
|
16
|
+
if sys.platform == "win32":
|
|
17
|
+
windowless = executable.with_name("pythonw.exe")
|
|
18
|
+
if windowless.exists():
|
|
19
|
+
executable = windowless
|
|
20
|
+
return [str(executable), "-m", "prkit"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_enabled() -> bool:
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
import winreg
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY) as key:
|
|
29
|
+
winreg.QueryValueEx(key, "PRKit")
|
|
30
|
+
return True
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
return False
|
|
33
|
+
return _startup_path().exists()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_enabled(enabled: bool) -> None:
|
|
37
|
+
if sys.platform == "win32":
|
|
38
|
+
_set_windows(enabled)
|
|
39
|
+
else:
|
|
40
|
+
path = _startup_path()
|
|
41
|
+
if not enabled:
|
|
42
|
+
path.unlink(missing_ok=True)
|
|
43
|
+
else:
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
if sys.platform == "darwin":
|
|
46
|
+
content = plistlib.dumps(
|
|
47
|
+
{
|
|
48
|
+
"Label": "com.codemee.prkit",
|
|
49
|
+
"ProgramArguments": launch_command(),
|
|
50
|
+
"RunAtLoad": True,
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
path.write_bytes(content)
|
|
54
|
+
else:
|
|
55
|
+
command = " ".join(_desktop_quote(arg) for arg in launch_command())
|
|
56
|
+
path.write_text(
|
|
57
|
+
"[Desktop Entry]\nType=Application\nName=PRKit\n"
|
|
58
|
+
f"Exec={command}\nTerminal=false\nX-GNOME-Autostart-enabled=true\n",
|
|
59
|
+
encoding="utf-8",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _set_windows(enabled: bool) -> None:
|
|
67
|
+
import subprocess
|
|
68
|
+
import winreg
|
|
69
|
+
|
|
70
|
+
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, RUN_KEY) as key:
|
|
71
|
+
if enabled:
|
|
72
|
+
command = subprocess.list2cmdline(launch_command())
|
|
73
|
+
winreg.SetValueEx(key, "PRKit", 0, winreg.REG_SZ, command)
|
|
74
|
+
else:
|
|
75
|
+
with suppress(FileNotFoundError):
|
|
76
|
+
winreg.DeleteValue(key, "PRKit")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _startup_path() -> Path:
|
|
80
|
+
if sys.platform == "darwin":
|
|
81
|
+
return Path.home() / "Library" / "LaunchAgents" / "com.codemee.prkit.plist"
|
|
82
|
+
config = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
83
|
+
return config / "autostart" / "prkit.desktop"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _desktop_quote(argument: str) -> str:
|
|
87
|
+
# Desktop Entry Exec uses double quotes, rather than shell quoting.
|
|
88
|
+
escaped = argument.replace("\\", "\\\\")
|
|
89
|
+
escaped = escaped.replace('"', '\\"').replace("$", "\\$").replace("`", "\\`")
|
|
90
|
+
return f'"{escaped}"'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from PySide6.QtCore import QByteArray
|
|
4
|
-
from PySide6.QtGui import QIcon, QPainter, QPixmap
|
|
4
|
+
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap
|
|
5
5
|
from PySide6.QtSvg import QSvgRenderer
|
|
6
6
|
|
|
7
7
|
TRAY_ICON_SVG = b"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
@@ -23,3 +23,24 @@ def tray_icon() -> QIcon:
|
|
|
23
23
|
painter.end()
|
|
24
24
|
icon.addPixmap(pixmap)
|
|
25
25
|
return icon
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def language_icon(mode: str) -> QIcon:
|
|
29
|
+
pixmap = QPixmap(32, 32)
|
|
30
|
+
pixmap.fill("transparent")
|
|
31
|
+
painter = QPainter(pixmap)
|
|
32
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
33
|
+
painter.setPen(QPen(QColor("#202124"), 2))
|
|
34
|
+
if mode == "system":
|
|
35
|
+
painter.drawEllipse(3, 3, 26, 26)
|
|
36
|
+
painter.drawEllipse(10, 3, 12, 26)
|
|
37
|
+
painter.drawLine(4, 16, 28, 16)
|
|
38
|
+
painter.drawLine(7, 9, 25, 9)
|
|
39
|
+
painter.drawLine(7, 23, 25, 23)
|
|
40
|
+
else:
|
|
41
|
+
font = QFont("Arial", 19 if mode == "zh" else 15)
|
|
42
|
+
font.setBold(True)
|
|
43
|
+
painter.setFont(font)
|
|
44
|
+
painter.drawText(pixmap.rect(), 0x84, "中" if mode == "zh" else "En")
|
|
45
|
+
painter.end()
|
|
46
|
+
return QIcon(pixmap)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import QLocale
|
|
4
|
+
|
|
5
|
+
ENGLISH = {
|
|
6
|
+
"簡報瑞士刀": "PRKit",
|
|
7
|
+
"選擇顏色": "Choose color",
|
|
8
|
+
"啟用聚光燈游標": "Enable spotlight cursor",
|
|
9
|
+
"聚光燈游標": "Spotlight cursor",
|
|
10
|
+
"顏色": "Color",
|
|
11
|
+
"透明度": "Opacity",
|
|
12
|
+
"圓圈大小": "Circle size",
|
|
13
|
+
"啟用按鍵顯示": "Enable keystroke display",
|
|
14
|
+
"按鍵顯示": "Keystroke display",
|
|
15
|
+
"Caps Lock/輸入法狀態": "Caps Lock / input source",
|
|
16
|
+
"啟用狀態顯示": "Enable status display",
|
|
17
|
+
" 秒": " sec",
|
|
18
|
+
"一直顯示": "Always show",
|
|
19
|
+
"狀態變化後的顯示時間;0 表示一直顯示": "Display time after a change; 0 means always show",
|
|
20
|
+
"Caps Lock 指示燈": "Caps Lock indicator",
|
|
21
|
+
"目前輸入法(中文/英文)": "Current input source (Chinese / English)",
|
|
22
|
+
"顯示內容": "Display content",
|
|
23
|
+
"背景顏色": "Background color",
|
|
24
|
+
"顯示時間": "Display time",
|
|
25
|
+
"顯示時可用滑鼠拖曳,位置會自動保存。": "Drag the indicator to move it. Its position is saved.",
|
|
26
|
+
"⚠ Wayland 不保證支援全域按鍵與覆蓋層": "⚠ Wayland may not support global keys or overlays",
|
|
27
|
+
"拖曳以移動狀態提示": "Drag to move the status indicator",
|
|
28
|
+
"開機自動啟動": "Start at login",
|
|
29
|
+
"語言": "Language",
|
|
30
|
+
"隨系統": "System default",
|
|
31
|
+
"中文": "Chinese",
|
|
32
|
+
"英文": "English",
|
|
33
|
+
"中文(注音)": "Chinese (Zhuyin)",
|
|
34
|
+
"結束": "Quit",
|
|
35
|
+
"無法設定開機自動啟動:{error}": "Could not change start at login: {error}",
|
|
36
|
+
"無法顯示鍵盤狀態:{message}": "Could not display keyboard status: {message}",
|
|
37
|
+
"無法啟用聚光燈游標:{error}": "Could not enable spotlight cursor: {error}",
|
|
38
|
+
"無法更新聚光燈游標:{error}": "Could not update spotlight cursor: {error}",
|
|
39
|
+
"無法啟用按鍵顯示:{error}": "Could not enable keystroke display: {error}",
|
|
40
|
+
"未知錯誤": "Unknown error",
|
|
41
|
+
"按鍵監聽已停止:{message}": "Keystroke monitoring stopped: {message}",
|
|
42
|
+
"此桌面環境沒有可用的系統匣。": "No system tray is available in this desktop environment.",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def effective_language(preference: str) -> str:
|
|
47
|
+
if preference == "system":
|
|
48
|
+
return "zh" if QLocale.system().language() == QLocale.Language.Chinese else "en"
|
|
49
|
+
return preference if preference in {"zh", "en"} else "zh"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def tr(text: str, language: str) -> str:
|
|
53
|
+
return ENGLISH.get(text, text) if language == "en" else text
|
|
@@ -51,6 +51,7 @@ class AppSettings:
|
|
|
51
51
|
spotlight: SpotlightSettings
|
|
52
52
|
keystroke: KeystrokeSettings
|
|
53
53
|
capslock: CapsLockSettings = field(default_factory=CapsLockSettings)
|
|
54
|
+
language: str = "system"
|
|
54
55
|
|
|
55
56
|
@classmethod
|
|
56
57
|
def defaults(cls) -> AppSettings:
|
|
@@ -110,10 +111,14 @@ class SettingsStore:
|
|
|
110
111
|
else "capslock"
|
|
111
112
|
),
|
|
112
113
|
)
|
|
113
|
-
|
|
114
|
+
language = backend.value("language", "system")
|
|
115
|
+
if language not in {"system", "zh", "en"}:
|
|
116
|
+
language = "system"
|
|
117
|
+
return AppSettings(spotlight, keystroke, capslock, language)
|
|
114
118
|
|
|
115
119
|
def save(self, settings: AppSettings) -> None:
|
|
116
120
|
values = {
|
|
121
|
+
"language": settings.language,
|
|
117
122
|
"spotlight/enabled": settings.spotlight.enabled,
|
|
118
123
|
"spotlight/color": settings.spotlight.color,
|
|
119
124
|
"spotlight/opacity": settings.spotlight.opacity,
|
|
@@ -22,7 +22,8 @@ from PySide6.QtWidgets import (
|
|
|
22
22
|
QWidgetAction,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
from prkit.icon import tray_icon
|
|
25
|
+
from prkit.icon import language_icon, tray_icon
|
|
26
|
+
from prkit.language import effective_language, tr
|
|
26
27
|
from prkit.platform import is_wayland
|
|
27
28
|
from prkit.settings import (
|
|
28
29
|
MAX_SPOTLIGHT_SIZE,
|
|
@@ -37,6 +38,7 @@ class ColorButton(QPushButton):
|
|
|
37
38
|
def __init__(self, color: str, parent: QWidget | None = None) -> None:
|
|
38
39
|
super().__init__(parent)
|
|
39
40
|
self._color = color
|
|
41
|
+
self._dialog_title = "選擇顏色"
|
|
40
42
|
self.setFixedSize(42, 24)
|
|
41
43
|
self.setToolTip("選擇顏色")
|
|
42
44
|
self.clicked.connect(self._choose_color)
|
|
@@ -54,7 +56,7 @@ class ColorButton(QPushButton):
|
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
def _choose_color(self) -> None:
|
|
57
|
-
chosen = QColorDialog.getColor(QColor(self._color), self,
|
|
59
|
+
chosen = QColorDialog.getColor(QColor(self._color), self, self._dialog_title)
|
|
58
60
|
if chosen.isValid():
|
|
59
61
|
self.set_color(chosen.name())
|
|
60
62
|
self.color_changed.emit(self._color)
|
|
@@ -126,6 +128,7 @@ class SettingsPanel(QWidget):
|
|
|
126
128
|
layout.setSpacing(10)
|
|
127
129
|
|
|
128
130
|
self.spotlight_enabled = QCheckBox("啟用聚光燈游標")
|
|
131
|
+
self.spotlight_enabled.setProperty("i18n", "啟用聚光燈游標")
|
|
129
132
|
self.spotlight_enabled.setChecked(settings.spotlight.enabled)
|
|
130
133
|
self.spotlight_color = ColorButton(settings.spotlight.color)
|
|
131
134
|
self.spotlight_opacity = ValueSlider(10, 100, settings.spotlight.opacity, "%")
|
|
@@ -144,6 +147,7 @@ class SettingsPanel(QWidget):
|
|
|
144
147
|
layout.addWidget(self._separator())
|
|
145
148
|
|
|
146
149
|
self.keystroke_enabled = QCheckBox("啟用按鍵顯示")
|
|
150
|
+
self.keystroke_enabled.setProperty("i18n", "啟用按鍵顯示")
|
|
147
151
|
self.keystroke_enabled.setChecked(settings.keystroke.enabled)
|
|
148
152
|
self.keystroke_color = ColorButton(settings.keystroke.color)
|
|
149
153
|
self.keystroke_opacity = ValueSlider(10, 100, settings.keystroke.opacity, "%")
|
|
@@ -155,6 +159,7 @@ class SettingsPanel(QWidget):
|
|
|
155
159
|
layout.addWidget(self._separator())
|
|
156
160
|
layout.addWidget(self._section_title("Caps Lock/輸入法狀態"))
|
|
157
161
|
self.capslock_enabled = QCheckBox("啟用狀態顯示")
|
|
162
|
+
self.capslock_enabled.setProperty("i18n", "啟用狀態顯示")
|
|
158
163
|
self.capslock_enabled.setChecked(settings.capslock.enabled)
|
|
159
164
|
self.capslock_color = ColorButton(settings.capslock.color)
|
|
160
165
|
self.capslock_opacity = ValueSlider(10, 100, settings.capslock.opacity, "%")
|
|
@@ -176,11 +181,13 @@ class SettingsPanel(QWidget):
|
|
|
176
181
|
layout.addLayout(self._row("透明度", self.capslock_opacity))
|
|
177
182
|
layout.addLayout(self._row("顯示時間", self.capslock_duration))
|
|
178
183
|
hint = QLabel("顯示時可用滑鼠拖曳,位置會自動保存。")
|
|
184
|
+
hint.setProperty("i18n", hint.text())
|
|
179
185
|
hint.setWordWrap(True)
|
|
180
186
|
layout.addWidget(hint)
|
|
181
187
|
|
|
182
188
|
if is_wayland():
|
|
183
189
|
warning = QLabel("⚠ Wayland 不保證支援全域按鍵與覆蓋層")
|
|
190
|
+
warning.setProperty("i18n", warning.text())
|
|
184
191
|
warning.setWordWrap(True)
|
|
185
192
|
warning.setStyleSheet("color: #b26a00; padding-top: 6px")
|
|
186
193
|
layout.addWidget(warning)
|
|
@@ -199,6 +206,22 @@ class SettingsPanel(QWidget):
|
|
|
199
206
|
self.capslock_mode.currentIndexChanged.connect(
|
|
200
207
|
lambda index: self.capslock_mode_changed.emit(self.capslock_mode.itemData(index))
|
|
201
208
|
)
|
|
209
|
+
self.retranslate(effective_language(settings.language))
|
|
210
|
+
|
|
211
|
+
def retranslate(self, language: str) -> None:
|
|
212
|
+
for widget in self.findChildren(QLabel) + self.findChildren(QCheckBox):
|
|
213
|
+
key = widget.property("i18n")
|
|
214
|
+
if key:
|
|
215
|
+
widget.setText(tr(key, language))
|
|
216
|
+
for button in self.findChildren(ColorButton):
|
|
217
|
+
button._dialog_title = tr("選擇顏色", language)
|
|
218
|
+
button.setToolTip(button._dialog_title)
|
|
219
|
+
self.capslock_duration.setSuffix(tr(" 秒", language))
|
|
220
|
+
self.capslock_duration.setSpecialValueText(tr("一直顯示", language))
|
|
221
|
+
self.capslock_duration.setToolTip(tr("狀態變化後的顯示時間;0 表示一直顯示", language))
|
|
222
|
+
self.capslock_mode.setItemText(0, tr("Caps Lock 指示燈", language))
|
|
223
|
+
if self.capslock_mode.count() > 1:
|
|
224
|
+
self.capslock_mode.setItemText(1, tr("目前輸入法(中文/英文)", language))
|
|
202
225
|
|
|
203
226
|
def set_spotlight_enabled(self, enabled: bool) -> None:
|
|
204
227
|
self.spotlight_enabled.setChecked(enabled)
|
|
@@ -209,6 +232,7 @@ class SettingsPanel(QWidget):
|
|
|
209
232
|
@staticmethod
|
|
210
233
|
def _section_title(text: str) -> QLabel:
|
|
211
234
|
label = QLabel(text)
|
|
235
|
+
label.setProperty("i18n", text)
|
|
212
236
|
font = label.font()
|
|
213
237
|
font.setBold(True)
|
|
214
238
|
label.setFont(font)
|
|
@@ -226,6 +250,7 @@ class SettingsPanel(QWidget):
|
|
|
226
250
|
layout = QHBoxLayout()
|
|
227
251
|
layout.setContentsMargins(18, 0, 0, 0)
|
|
228
252
|
title = QLabel(label)
|
|
253
|
+
title.setProperty("i18n", label)
|
|
229
254
|
title.setMinimumWidth(62)
|
|
230
255
|
layout.addWidget(title)
|
|
231
256
|
layout.addWidget(widget, 1)
|
|
@@ -234,6 +259,8 @@ class SettingsPanel(QWidget):
|
|
|
234
259
|
|
|
235
260
|
class TrayController(QObject):
|
|
236
261
|
exit_requested = Signal()
|
|
262
|
+
autostart_changed = Signal(bool)
|
|
263
|
+
language_changed = Signal(str)
|
|
237
264
|
|
|
238
265
|
def __init__(self, settings: AppSettings, parent: QObject | None = None) -> None:
|
|
239
266
|
super().__init__(parent)
|
|
@@ -243,12 +270,59 @@ class TrayController(QObject):
|
|
|
243
270
|
panel_action.setDefaultWidget(self.panel)
|
|
244
271
|
self.menu.addAction(panel_action)
|
|
245
272
|
self.menu.addSeparator()
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
autostart_row = QWidget(self.menu)
|
|
274
|
+
autostart_layout = QHBoxLayout(autostart_row)
|
|
275
|
+
autostart_layout.setContentsMargins(14, 4, 14, 4)
|
|
276
|
+
self.autostart_checkbox = QCheckBox(autostart_row)
|
|
277
|
+
self.autostart_checkbox.toggled.connect(self.autostart_changed)
|
|
278
|
+
autostart_layout.addWidget(self.autostart_checkbox)
|
|
279
|
+
autostart_action = QWidgetAction(self.menu)
|
|
280
|
+
autostart_action.setDefaultWidget(autostart_row)
|
|
281
|
+
self.menu.addAction(autostart_action)
|
|
282
|
+
self.language_mode = settings.language
|
|
283
|
+
language_row = QWidget(self.menu)
|
|
284
|
+
language_layout = QHBoxLayout(language_row)
|
|
285
|
+
language_layout.setContentsMargins(14, 4, 14, 4)
|
|
286
|
+
self.language_label = QLabel(language_row)
|
|
287
|
+
self.language_button = QPushButton(language_row)
|
|
288
|
+
self.language_button.setFixedSize(40, 36)
|
|
289
|
+
self.language_button.clicked.connect(self._cycle_language)
|
|
290
|
+
language_layout.addWidget(self.language_label)
|
|
291
|
+
language_layout.addStretch(1)
|
|
292
|
+
language_layout.addWidget(self.language_button)
|
|
293
|
+
language_action = QWidgetAction(self.menu)
|
|
294
|
+
language_action.setDefaultWidget(language_row)
|
|
295
|
+
self.menu.addAction(language_action)
|
|
296
|
+
self.exit_action = QAction(self.menu)
|
|
297
|
+
self.exit_action.triggered.connect(self.exit_requested)
|
|
298
|
+
self.menu.addAction(self.exit_action)
|
|
249
299
|
self.tray = QSystemTrayIcon(tray_icon(), self)
|
|
250
|
-
self.tray.setToolTip("簡報瑞士刀")
|
|
251
300
|
self.tray.setContextMenu(self.menu)
|
|
301
|
+
self.retranslate(effective_language(settings.language))
|
|
302
|
+
|
|
303
|
+
def retranslate(self, language: str) -> None:
|
|
304
|
+
self.panel.retranslate(language)
|
|
305
|
+
self.autostart_checkbox.setText(tr("開機自動啟動", language))
|
|
306
|
+
mode_label = {"system": "隨系統", "zh": "中文", "en": "英文"}[self.language_mode]
|
|
307
|
+
separator = ": " if language == "en" else ":"
|
|
308
|
+
self.language_label.setText(f"{tr('語言', language)}{separator}{tr(mode_label, language)}")
|
|
309
|
+
self.language_button.setIcon(language_icon(self.language_mode))
|
|
310
|
+
self.language_button.setToolTip(self.language_label.text())
|
|
311
|
+
self.exit_action.setText(tr("結束", language))
|
|
312
|
+
self.tray.setToolTip(tr("簡報瑞士刀", language))
|
|
313
|
+
|
|
314
|
+
def set_language(self, preference: str) -> None:
|
|
315
|
+
self.language_mode = preference
|
|
316
|
+
self.retranslate(effective_language(preference))
|
|
317
|
+
|
|
318
|
+
def _cycle_language(self) -> None:
|
|
319
|
+
modes = ("system", "zh", "en")
|
|
320
|
+
self.language_changed.emit(modes[(modes.index(self.language_mode) + 1) % len(modes)])
|
|
321
|
+
|
|
322
|
+
def set_autostart_checked(self, enabled: bool) -> None:
|
|
323
|
+
self.autostart_checkbox.blockSignals(True)
|
|
324
|
+
self.autostart_checkbox.setChecked(enabled)
|
|
325
|
+
self.autostart_checkbox.blockSignals(False)
|
|
252
326
|
|
|
253
327
|
@staticmethod
|
|
254
328
|
def is_available() -> bool:
|
|
@@ -262,7 +336,9 @@ class TrayController(QObject):
|
|
|
262
336
|
self.tray.hide()
|
|
263
337
|
|
|
264
338
|
def notify_warning(self, message: str) -> None:
|
|
265
|
-
self.tray.showMessage(
|
|
339
|
+
self.tray.showMessage(
|
|
340
|
+
self.tray.toolTip(), message, QSystemTrayIcon.MessageIcon.Warning, 5000
|
|
341
|
+
)
|
|
266
342
|
|
|
267
343
|
def connect_settings(
|
|
268
344
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|