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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prkit
3
- Version: 0.2.1
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.2.1-windows-x64.exe` | 直接開啟執行 |
40
- | macOS Apple Silicon | `PRKit-0.2.1-macos-arm64.dmg` | 開啟 DMG,將 `PRKit.app` 拖到「Applications」捷徑後啟動 |
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 雲端打包與成品測試;手動建置可在 Actions 的 **Build desktop apps** 執行,成品在該次執行的 Artifacts。詳細操作見[發佈指南](docs/RELEASING.md)。
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.2.1`。
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.2.1-windows-x64.exe` | 直接開啟執行 |
23
- | macOS Apple Silicon | `PRKit-0.2.1-macos-arm64.dmg` | 開啟 DMG,將 `PRKit.app` 拖到「Applications」捷徑後啟動 |
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 雲端打包與成品測試;手動建置可在 Actions 的 **Build desktop apps** 執行,成品在該次執行的 Artifacts。詳細操作見[發佈指南](docs/RELEASING.md)。
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.2.1`。
56
+ 若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.3.0`。
55
57
 
56
58
  若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
57
59
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prkit"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "A cross-platform system-tray presentation toolkit"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prkit"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "A cross-platform system-tray presentation toolkit"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,3 +1,3 @@
1
1
  """Presentation tools for the system tray."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"
@@ -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(f"無法顯示鍵盤狀態:{message}")
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(f"無法啟用聚光燈游標:{error}")
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(f"無法更新聚光燈游標:{error}")
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(f"無法啟用按鍵顯示:{error or '未知錯誤'}")
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(f"按鍵監聽已停止:{message}")
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
- QMessageBox.critical(None, "簡報瑞士刀", "此桌面環境沒有可用的系統匣。")
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
- return AppSettings(spotlight, keystroke, capslock)
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
- exit_action = QAction("結束", self.menu)
247
- exit_action.triggered.connect(self.exit_requested)
248
- self.menu.addAction(exit_action)
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("簡報瑞士刀", message, QSystemTrayIcon.MessageIcon.Warning, 5000)
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