frontengine 1.0.31__tar.gz → 1.0.32__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.

Potentially problematic release.


This version of frontengine might be problematic. Click here for more details.

Files changed (165) hide show
  1. {frontengine-1.0.31 → frontengine-1.0.32}/PKG-INFO +1 -1
  2. frontengine-1.0.32/frontengine/show/presentation/annotation_overlay.py +153 -0
  3. frontengine-1.0.32/frontengine/show/presentation/cursor_effects.py +166 -0
  4. frontengine-1.0.32/frontengine/show/presentation/keystroke_display.py +161 -0
  5. frontengine-1.0.32/frontengine/show/presentation/magnifier.py +127 -0
  6. frontengine-1.0.32/frontengine/show/screen_care/screen_filter.py +147 -0
  7. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/main_ui.py +6 -0
  8. frontengine-1.0.32/frontengine/ui/page/presentation/presentation_setting_ui.py +365 -0
  9. frontengine-1.0.32/frontengine/ui/page/screen_care/screen_care_setting_ui.py +295 -0
  10. frontengine-1.0.32/frontengine/utils/break_reminder/break_reminder.py +104 -0
  11. frontengine-1.0.32/frontengine/utils/input_watch/input_watch_service.py +130 -0
  12. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/english.py +41 -0
  13. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/traditional_chinese.py +41 -0
  14. frontengine-1.0.32/frontengine/utils/smart_pause/__init__.py +0 -0
  15. frontengine-1.0.32/frontengine/utils/system_stats/__init__.py +0 -0
  16. frontengine-1.0.32/frontengine/utils/text_source/__init__.py +0 -0
  17. frontengine-1.0.32/frontengine/utils/theme_schedule/__init__.py +0 -0
  18. frontengine-1.0.32/frontengine/utils/weather/__init__.py +0 -0
  19. frontengine-1.0.32/frontengine/worker/__init__.py +0 -0
  20. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/PKG-INFO +1 -1
  21. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/SOURCES.txt +15 -0
  22. {frontengine-1.0.31 → frontengine-1.0.32}/pyproject.toml +1 -1
  23. {frontengine-1.0.31 → frontengine-1.0.32}/LICENSE +0 -0
  24. {frontengine-1.0.31 → frontengine-1.0.32}/README.md +0 -0
  25. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/__init__.py +0 -0
  26. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/__main__.py +0 -0
  27. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/__init__.py +0 -0
  28. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/base_widget.py +0 -0
  29. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/gif/__init__.py +0 -0
  30. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/gif/paint_gif.py +0 -0
  31. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/image/__init__.py +0 -0
  32. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/image/paint_image.py +0 -0
  33. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/load/__init__.py +0 -0
  34. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/load/load_someone_make_ui.py +0 -0
  35. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/overlay_factory.py +0 -0
  36. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/particle/__init__.py +0 -0
  37. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/particle/particle_ui.py +0 -0
  38. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/pet/__init__.py +0 -0
  39. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/pet/desktop_pet.py +0 -0
  40. {frontengine-1.0.31/frontengine/show/scene → frontengine-1.0.32/frontengine/show/presentation}/__init__.py +0 -0
  41. {frontengine-1.0.31/frontengine/show/sound_player → frontengine-1.0.32/frontengine/show/scene}/__init__.py +0 -0
  42. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  43. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/scene/extend_graphic_view.py +0 -0
  44. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/scene/scene.py +0 -0
  45. {frontengine-1.0.31/frontengine/show/text → frontengine-1.0.32/frontengine/show/screen_care}/__init__.py +0 -0
  46. {frontengine-1.0.31/frontengine/show/video → frontengine-1.0.32/frontengine/show/sound_player}/__init__.py +0 -0
  47. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/sound_player/sound_effect.py +0 -0
  48. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/sound_player/sound_player.py +0 -0
  49. {frontengine-1.0.31/frontengine/show/web → frontengine-1.0.32/frontengine/show/text}/__init__.py +0 -0
  50. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/text/draw_text.py +0 -0
  51. {frontengine-1.0.31/frontengine/system_tray → frontengine-1.0.32/frontengine/show/video}/__init__.py +0 -0
  52. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/video/video_player.py +0 -0
  53. {frontengine-1.0.31/frontengine/ui → frontengine-1.0.32/frontengine/show/web}/__init__.py +0 -0
  54. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/web/webview.py +0 -0
  55. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/window_helpers.py +0 -0
  56. {frontengine-1.0.31/frontengine/ui/color → frontengine-1.0.32/frontengine/system_tray}/__init__.py +0 -0
  57. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/system_tray/extend_system_tray.py +0 -0
  58. {frontengine-1.0.31/frontengine/ui/dialog → frontengine-1.0.32/frontengine/ui}/__init__.py +0 -0
  59. {frontengine-1.0.31/frontengine/ui/menu → frontengine-1.0.32/frontengine/ui/color}/__init__.py +0 -0
  60. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/color/global_color.py +0 -0
  61. {frontengine-1.0.31/frontengine/ui/page → frontengine-1.0.32/frontengine/ui/dialog}/__init__.py +0 -0
  62. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  63. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  64. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  65. {frontengine-1.0.31/frontengine/ui/page/control_center → frontengine-1.0.32/frontengine/ui/menu}/__init__.py +0 -0
  66. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/help_menu.py +0 -0
  67. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/how_to_menu.py +0 -0
  68. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/language_menu.py +0 -0
  69. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/preset_menu.py +0 -0
  70. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/settings_menu.py +0 -0
  71. {frontengine-1.0.31/frontengine/ui/page/gif → frontengine-1.0.32/frontengine/ui/page}/__init__.py +0 -0
  72. {frontengine-1.0.31/frontengine/ui/page/image → frontengine-1.0.32/frontengine/ui/page/control_center}/__init__.py +0 -0
  73. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  74. {frontengine-1.0.31/frontengine/ui/page/particle → frontengine-1.0.32/frontengine/ui/page/gif}/__init__.py +0 -0
  75. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  76. {frontengine-1.0.31/frontengine/ui/page/pet → frontengine-1.0.32/frontengine/ui/page/image}/__init__.py +0 -0
  77. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  78. {frontengine-1.0.31/frontengine/ui/page/scene_setting → frontengine-1.0.32/frontengine/ui/page/particle}/__init__.py +0 -0
  79. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  80. {frontengine-1.0.31/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.32/frontengine/ui/page/pet}/__init__.py +0 -0
  81. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
  82. {frontengine-1.0.31/frontengine/ui/page/sound_player → frontengine-1.0.32/frontengine/ui/page/presentation}/__init__.py +0 -0
  83. {frontengine-1.0.31/frontengine/ui/page/text → frontengine-1.0.32/frontengine/ui/page/scene_setting}/__init__.py +0 -0
  84. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  85. {frontengine-1.0.31/frontengine/ui/page/video → frontengine-1.0.32/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
  86. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  87. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  88. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  89. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  90. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  91. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  92. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  93. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  94. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  95. {frontengine-1.0.31/frontengine/ui/page/web → frontengine-1.0.32/frontengine/ui/page/screen_care}/__init__.py +0 -0
  96. {frontengine-1.0.31/frontengine/user_setting → frontengine-1.0.32/frontengine/ui/page/sound_player}/__init__.py +0 -0
  97. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  98. {frontengine-1.0.31/frontengine/utils → frontengine-1.0.32/frontengine/ui/page/text}/__init__.py +0 -0
  99. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  100. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/utils.py +0 -0
  101. {frontengine-1.0.31/frontengine/utils/audio_meter → frontengine-1.0.32/frontengine/ui/page/video}/__init__.py +0 -0
  102. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  103. {frontengine-1.0.31/frontengine/utils/browser → frontengine-1.0.32/frontengine/ui/page/web}/__init__.py +0 -0
  104. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  105. {frontengine-1.0.31/frontengine/utils/critical_exit → frontengine-1.0.32/frontengine/user_setting}/__init__.py +0 -0
  106. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/user_setting/preset_repository.py +0 -0
  107. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/user_setting/scene_setting.py +0 -0
  108. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/user_setting/user_setting_file.py +0 -0
  109. {frontengine-1.0.31/frontengine/utils/file → frontengine-1.0.32/frontengine/utils}/__init__.py +0 -0
  110. {frontengine-1.0.31/frontengine/utils/focus_timer → frontengine-1.0.32/frontengine/utils/audio_meter}/__init__.py +0 -0
  111. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  112. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  113. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  114. {frontengine-1.0.31/frontengine/utils/hotkey → frontengine-1.0.32/frontengine/utils/break_reminder}/__init__.py +0 -0
  115. {frontengine-1.0.31/frontengine/utils/json → frontengine-1.0.32/frontengine/utils/browser}/__init__.py +0 -0
  116. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/browser/browser.py +0 -0
  117. {frontengine-1.0.31/frontengine/utils/logging → frontengine-1.0.32/frontengine/utils/critical_exit}/__init__.py +0 -0
  118. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  119. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  120. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  121. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/exception/__init__.py +0 -0
  122. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/exception/exception_tags.py +0 -0
  123. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/exception/exceptions.py +0 -0
  124. {frontengine-1.0.31/frontengine/utils/multi_language → frontengine-1.0.32/frontengine/utils/file}/__init__.py +0 -0
  125. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/open/__init__.py +0 -0
  126. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/open/open_file.py +0 -0
  127. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/save/__init__.py +0 -0
  128. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/save/save_file.py +0 -0
  129. {frontengine-1.0.31/frontengine/utils/pet_chat → frontengine-1.0.32/frontengine/utils/focus_timer}/__init__.py +0 -0
  130. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/focus_timer/focus_timer.py +0 -0
  131. {frontengine-1.0.31/frontengine/utils/preset_schedule → frontengine-1.0.32/frontengine/utils/hotkey}/__init__.py +0 -0
  132. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  133. {frontengine-1.0.31/frontengine/utils/redirect_manager → frontengine-1.0.32/frontengine/utils/input_watch}/__init__.py +0 -0
  134. {frontengine-1.0.31/frontengine/utils/smart_pause → frontengine-1.0.32/frontengine/utils/json}/__init__.py +0 -0
  135. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json/json_file.py +0 -0
  136. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json/json_repository.py +0 -0
  137. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json_format/__init__.py +0 -0
  138. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json_format/json_process.py +0 -0
  139. {frontengine-1.0.31/frontengine/utils/system_stats → frontengine-1.0.32/frontengine/utils/logging}/__init__.py +0 -0
  140. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/logging/loggin_instance.py +0 -0
  141. {frontengine-1.0.31/frontengine/utils/text_source → frontengine-1.0.32/frontengine/utils/multi_language}/__init__.py +0 -0
  142. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/france.py +0 -0
  143. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/germany.py +0 -0
  144. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/italy.py +0 -0
  145. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  146. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/russian.py +0 -0
  147. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  148. {frontengine-1.0.31/frontengine/utils/theme_schedule → frontengine-1.0.32/frontengine/utils/pet_chat}/__init__.py +0 -0
  149. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
  150. {frontengine-1.0.31/frontengine/utils/weather → frontengine-1.0.32/frontengine/utils/preset_schedule}/__init__.py +0 -0
  151. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  152. {frontengine-1.0.31/frontengine/worker → frontengine-1.0.32/frontengine/utils/redirect_manager}/__init__.py +0 -0
  153. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  154. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  155. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/system_stats/system_stats.py +0 -0
  156. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/text_source/text_source.py +0 -0
  157. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  158. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/weather/weather_service.py +0 -0
  159. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/web_url.py +0 -0
  160. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/worker/qthread_worker.py +0 -0
  161. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/dependency_links.txt +0 -0
  162. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/entry_points.txt +0 -0
  163. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/requires.txt +0 -0
  164. {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/top_level.txt +0 -0
  165. {frontengine-1.0.31 → frontengine-1.0.32}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.31
3
+ Version: 1.0.32
4
4
  Summary: FrontEngine is use-define frontview or only use like screen saver
5
5
  Author-email: JE-Chen <jechenmailman@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Intergration-Automation-Testing/FrontEngine
@@ -0,0 +1,153 @@
1
+ """
2
+ 螢幕塗鴉層:在畫面上用筆或螢光筆畫線、清除、復原。畫圖時需要接收滑鼠,
3
+ 所以它不是點擊穿透的;關閉塗鴉即恢復原本的操作。
4
+
5
+ A screen annotation layer: draw over the screen with a pen or highlighter,
6
+ undo, and clear. It has to take the mouse while you draw, so unlike the other
7
+ overlays it is not click-through — turn it off and the screen is yours again.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import List, Optional, Tuple
12
+
13
+ from PySide6.QtCore import QPoint, Qt
14
+ from PySide6.QtGui import QColor, QPainter, QPen
15
+
16
+ from frontengine.show.base_widget import BaseWidget
17
+ from frontengine.show.window_helpers import apply_overlay_window_flags
18
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
19
+
20
+ TOOL_PEN = "pen"
21
+ TOOL_HIGHLIGHTER = "highlighter"
22
+ TOOL_ERASER = "eraser"
23
+ TOOLS = (TOOL_PEN, TOOL_HIGHLIGHTER, TOOL_ERASER)
24
+
25
+ PEN_COLORS = (
26
+ ("Red", "#ff3b30"), ("Yellow", "#ffcc00"), ("Green", "#34c759"),
27
+ ("Blue", "#0a84ff"), ("Black", "#000000"), ("White", "#ffffff"),
28
+ )
29
+ DEFAULT_PEN_COLOR = "#ff3b30"
30
+ MIN_WIDTH = 1
31
+ MAX_WIDTH = 40
32
+
33
+
34
+ def clamp_width(value, fallback: int = 4) -> int:
35
+ """把筆寬夾在可用範圍。"""
36
+ try:
37
+ return max(MIN_WIDTH, min(MAX_WIDTH, int(value)))
38
+ except (TypeError, ValueError):
39
+ return fallback
40
+
41
+
42
+ def stroke_style(tool: str, color: str, width: int) -> Tuple[QColor, int]:
43
+ """
44
+ 依工具決定實際的顏色與線寬:螢光筆是半透明的粗線,橡皮擦畫得更粗。
45
+ Per-tool colour and width: a highlighter is a wide translucent stroke, an
46
+ eraser is wider still.
47
+ """
48
+ pen_color = QColor(color)
49
+ if not pen_color.isValid():
50
+ pen_color = QColor(DEFAULT_PEN_COLOR)
51
+ stroke_width = clamp_width(width)
52
+ if tool == TOOL_HIGHLIGHTER:
53
+ pen_color.setAlpha(90)
54
+ stroke_width = clamp_width(stroke_width * 4)
55
+ elif tool == TOOL_ERASER:
56
+ stroke_width = clamp_width(stroke_width * 5)
57
+ return (pen_color, stroke_width)
58
+
59
+
60
+ class AnnotationOverlay(BaseWidget):
61
+ """
62
+ 塗鴉層:滑鼠拖曳畫線,支援筆/螢光筆/橡皮擦、復原與全部清除。
63
+ 每一筆記成 (工具, 顏色, 線寬, 點清單),因此復原與重繪都很單純。
64
+ """
65
+
66
+ def __init__(self, color: str = DEFAULT_PEN_COLOR, width: int = 4, tool: str = TOOL_PEN) -> None:
67
+ front_engine_logger.info(f"[AnnotationOverlay] Init | tool={tool}, color={color}, width={width}")
68
+ super().__init__()
69
+ self.opacity = 1.0
70
+ self.tool = tool if tool in TOOLS else TOOL_PEN
71
+ self.pen_color = color
72
+ self.pen_width = clamp_width(width)
73
+ self.strokes: List[dict] = []
74
+ self._current: Optional[dict] = None
75
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
76
+
77
+ def set_annotation_window_flag(self) -> None:
78
+ """塗鴉需要接收滑鼠,所以不使用點擊穿透。"""
79
+ apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
80
+
81
+ def set_tool(self, tool: str) -> None:
82
+ if tool in TOOLS:
83
+ self.tool = tool
84
+
85
+ def set_pen(self, color: Optional[str] = None, width: Optional[int] = None) -> None:
86
+ if color is not None:
87
+ self.pen_color = color
88
+ if width is not None:
89
+ self.pen_width = clamp_width(width)
90
+
91
+ def clear(self) -> None:
92
+ """清掉所有筆畫 / Wipe every stroke."""
93
+ self.strokes = []
94
+ self._current = None
95
+ self.update()
96
+
97
+ def undo(self) -> bool:
98
+ """收回最後一筆;沒有筆畫可收時回傳 False。"""
99
+ if not self.strokes:
100
+ return False
101
+ self.strokes.pop()
102
+ self.update()
103
+ return True
104
+
105
+ # --- drawing ---------------------------------------------------------
106
+ def begin_stroke(self, point: QPoint) -> None:
107
+ self._current = {
108
+ "tool": self.tool, "color": self.pen_color, "width": self.pen_width, "points": [point],
109
+ }
110
+ self.strokes.append(self._current)
111
+
112
+ def extend_stroke(self, point: QPoint) -> None:
113
+ if self._current is not None:
114
+ self._current["points"].append(point)
115
+ self.update()
116
+
117
+ def end_stroke(self) -> None:
118
+ # 只有一個點的筆畫畫不出線,直接丟掉
119
+ # A single-point stroke draws nothing; drop it.
120
+ if self._current is not None and len(self._current["points"]) < 2:
121
+ self.strokes.remove(self._current)
122
+ self.update()
123
+ self._current = None
124
+
125
+ def mousePressEvent(self, event) -> None:
126
+ if event.button() == Qt.MouseButton.LeftButton:
127
+ self.begin_stroke(event.position().toPoint())
128
+ super().mousePressEvent(event)
129
+
130
+ def mouseMoveEvent(self, event) -> None:
131
+ self.extend_stroke(event.position().toPoint())
132
+ super().mouseMoveEvent(event)
133
+
134
+ def mouseReleaseEvent(self, event) -> None:
135
+ self.end_stroke()
136
+ super().mouseReleaseEvent(event)
137
+
138
+ def draw_content(self, painter: QPainter) -> None:
139
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
140
+ for stroke in self.strokes:
141
+ points = stroke["points"]
142
+ if len(points) < 2:
143
+ continue
144
+ color, width = stroke_style(stroke["tool"], stroke["color"], stroke["width"])
145
+ if stroke["tool"] == TOOL_ERASER:
146
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
147
+ else:
148
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
149
+ painter.setPen(QPen(color, width, Qt.PenStyle.SolidLine,
150
+ Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
151
+ for start, end in zip(points, points[1:]):
152
+ painter.drawLine(start, end)
153
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
@@ -0,0 +1,166 @@
1
+ """
2
+ 游標強調層:在游標周圍畫一圈亮環、點擊時擴散漣漪,或把游標以外的區域壓暗
3
+ (聚光燈)。教學、簡報與錄影時讓觀眾看得見滑鼠在哪。點擊穿透,不影響操作。
4
+
5
+ Cursor emphasis: a ring around the pointer, a ripple when you click, or a
6
+ spotlight that dims everything except a circle around the cursor — so an
7
+ audience can follow the mouse during a demo. Click-through throughout.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import List, Optional
12
+
13
+ from PySide6.QtCore import QPoint, QTimer, Qt
14
+ from PySide6.QtGui import QColor, QCursor, QPainter, QRadialGradient
15
+
16
+ from frontengine.show.base_widget import BaseWidget
17
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
18
+
19
+ DEFAULT_RING_COLOR = "#ffcc00"
20
+ DEFAULT_RING_RADIUS = 28
21
+ DEFAULT_SPOTLIGHT_RADIUS = 160
22
+ RIPPLE_STEPS = 12
23
+ _MIN_RADIUS = 8
24
+ _MAX_RADIUS = 600
25
+
26
+
27
+ def clamp_radius(value, fallback: int = DEFAULT_RING_RADIUS) -> int:
28
+ """把半徑夾在看得見又不至於蓋滿螢幕的範圍。"""
29
+ try:
30
+ return max(_MIN_RADIUS, min(_MAX_RADIUS, int(value)))
31
+ except (TypeError, ValueError):
32
+ return fallback
33
+
34
+
35
+ def ripple_radius(step: int, base_radius: int, steps: int = RIPPLE_STEPS) -> int:
36
+ """漣漪第 step 幀的半徑:從基準半徑往外擴散一倍。"""
37
+ total = max(1, int(steps))
38
+ progress = min(max(int(step), 0), total) / total
39
+ return int(clamp_radius(base_radius) * (1.0 + progress))
40
+
41
+
42
+ def ripple_alpha(step: int, steps: int = RIPPLE_STEPS) -> int:
43
+ """漣漪第 step 幀的透明度:越擴散越淡,最後消失。"""
44
+ total = max(1, int(steps))
45
+ progress = min(max(int(step), 0), total) / total
46
+ return max(0, int(200 * (1.0 - progress)))
47
+
48
+
49
+ class CursorEffectWidget(BaseWidget):
50
+ """
51
+ 游標效果層:亮環、點擊漣漪、聚光燈可各自開關,全部跟著游標移動。
52
+ 位置與點擊事件由外部注入(正式路徑接系統游標與全域滑鼠監聽),方便測試。
53
+ """
54
+
55
+ REFRESH_MS = 16
56
+
57
+ def __init__(self, ring: bool = True, ripple: bool = True, spotlight: bool = False,
58
+ color: str = DEFAULT_RING_COLOR, radius: int = DEFAULT_RING_RADIUS,
59
+ spotlight_radius: int = DEFAULT_SPOTLIGHT_RADIUS, dim: int = 55) -> None:
60
+ front_engine_logger.info(
61
+ f"[CursorEffectWidget] Init | ring={ring}, ripple={ripple}, spotlight={spotlight}")
62
+ super().__init__()
63
+ self.opacity = 1.0
64
+ self.show_ring = bool(ring)
65
+ self.show_ripple = bool(ripple)
66
+ self.show_spotlight = bool(spotlight)
67
+ self.color = QColor(color) if QColor(color).isValid() else QColor(DEFAULT_RING_COLOR)
68
+ self.radius = clamp_radius(radius)
69
+ self.spotlight_radius = clamp_radius(spotlight_radius, DEFAULT_SPOTLIGHT_RADIUS)
70
+ self.dim = max(0, min(90, int(dim)))
71
+ self.cursor_point = QPoint(0, 0)
72
+ self.ripples: List[dict] = []
73
+ self._cursor_provider = QCursor.pos
74
+ self._timer = QTimer(self)
75
+ self._timer.timeout.connect(self.tick)
76
+
77
+ def set_cursor_provider(self, provider) -> None:
78
+ """注入游標位置來源(測試用)。"""
79
+ self._cursor_provider = provider
80
+
81
+ def set_effects(self, ring: Optional[bool] = None, ripple: Optional[bool] = None,
82
+ spotlight: Optional[bool] = None) -> None:
83
+ """開關個別效果。"""
84
+ if ring is not None:
85
+ self.show_ring = bool(ring)
86
+ if ripple is not None:
87
+ self.show_ripple = bool(ripple)
88
+ if spotlight is not None:
89
+ self.show_spotlight = bool(spotlight)
90
+ self.update()
91
+
92
+ def set_style(self, color: Optional[str] = None, radius: Optional[int] = None,
93
+ spotlight_radius: Optional[int] = None, dim: Optional[int] = None) -> None:
94
+ """調整顏色、半徑與壓暗程度。"""
95
+ if color is not None:
96
+ candidate = QColor(color)
97
+ if candidate.isValid():
98
+ self.color = candidate
99
+ if radius is not None:
100
+ self.radius = clamp_radius(radius)
101
+ if spotlight_radius is not None:
102
+ self.spotlight_radius = clamp_radius(spotlight_radius, DEFAULT_SPOTLIGHT_RADIUS)
103
+ if dim is not None:
104
+ self.dim = max(0, min(90, int(dim)))
105
+ self.update()
106
+
107
+ def start_following(self, interval_ms: int = REFRESH_MS) -> None:
108
+ """開始跟隨游標。"""
109
+ self.tick()
110
+ self._timer.start(max(8, int(interval_ms)))
111
+
112
+ def add_ripple(self, point: Optional[QPoint] = None) -> None:
113
+ """在指定位置(預設為目前游標)加一圈點擊漣漪。"""
114
+ self.ripples.append({"point": QPoint(point or self.cursor_point), "step": 0})
115
+ self.update()
116
+
117
+ def tick(self) -> None:
118
+ """更新游標位置並推進漣漪動畫。"""
119
+ try:
120
+ position = self._cursor_provider()
121
+ except Exception: # pragma: no cover - defensive around injected providers
122
+ return
123
+ self.cursor_point = QPoint(int(position.x()) - self.x(), int(position.y()) - self.y())
124
+ for ripple in list(self.ripples):
125
+ ripple["step"] += 1
126
+ if ripple["step"] > RIPPLE_STEPS:
127
+ self.ripples.remove(ripple)
128
+ self.update()
129
+
130
+ def draw_content(self, painter: QPainter) -> None:
131
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
132
+ if self.show_spotlight:
133
+ self._draw_spotlight(painter)
134
+ if self.show_ring:
135
+ ring_color = QColor(self.color)
136
+ ring_color.setAlpha(120)
137
+ painter.setBrush(ring_color)
138
+ painter.setPen(Qt.PenStyle.NoPen)
139
+ painter.drawEllipse(self.cursor_point, self.radius, self.radius)
140
+ if self.show_ripple:
141
+ for ripple in self.ripples:
142
+ color = QColor(self.color)
143
+ color.setAlpha(ripple_alpha(ripple["step"]))
144
+ painter.setBrush(Qt.BrushStyle.NoBrush)
145
+ painter.setPen(color)
146
+ radius = ripple_radius(ripple["step"], self.radius)
147
+ painter.drawEllipse(ripple["point"], radius, radius)
148
+
149
+ def _draw_spotlight(self, painter: QPainter) -> None:
150
+ """把游標以外壓暗,中心保持透明,邊緣柔和過渡。"""
151
+ shade = QColor(0, 0, 0, int(255 * self.dim / 100.0))
152
+ gradient = QRadialGradient(self.cursor_point, self.spotlight_radius)
153
+ gradient.setColorAt(0.0, QColor(0, 0, 0, 0))
154
+ gradient.setColorAt(0.75, QColor(0, 0, 0, 0))
155
+ gradient.setColorAt(1.0, shade)
156
+ painter.fillRect(self.rect(), shade)
157
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
158
+ painter.setBrush(gradient)
159
+ painter.setPen(Qt.PenStyle.NoPen)
160
+ painter.drawEllipse(self.cursor_point, self.spotlight_radius, self.spotlight_radius)
161
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
162
+
163
+ def closeEvent(self, event) -> None:
164
+ if self._timer.isActive():
165
+ self._timer.stop()
166
+ super().closeEvent(event)
@@ -0,0 +1,161 @@
1
+ """
2
+ 按鍵顯示:把剛按下的按鍵顯示在畫面角落,做教學影片或直播時觀眾才知道你按了什麼。
3
+ PowerToys 至今沒有內建這個功能,而本專案本來就有全域鍵盤監聽(pynput),
4
+ 所以只需要一層顯示。按鍵格式化是純函式,可獨立測試。
5
+
6
+ Keystroke display: show what you just pressed in a screen corner, so viewers of
7
+ a tutorial or stream can follow along. PowerToys still has no built-in version;
8
+ this project already listens for global hotkeys, so only the display is new.
9
+ The formatting is pure and independently testable.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import time
14
+ from typing import List, Tuple
15
+
16
+ from PySide6.QtCore import QRect, QTimer, Qt
17
+ from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter
18
+
19
+ from frontengine.show.base_widget import BaseWidget
20
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
21
+
22
+ DEFAULT_HOLD_SECONDS = 2.0
23
+ MAX_KEYS_SHOWN = 6
24
+
25
+ # 修飾鍵的顯示名稱 / How modifier keys are shown
26
+ _MODIFIER_NAMES = {
27
+ "ctrl": "Ctrl", "ctrl_l": "Ctrl", "ctrl_r": "Ctrl",
28
+ "alt": "Alt", "alt_l": "Alt", "alt_r": "Alt", "alt_gr": "AltGr",
29
+ "shift": "Shift", "shift_l": "Shift", "shift_r": "Shift",
30
+ "cmd": "Win", "cmd_l": "Win", "cmd_r": "Win",
31
+ }
32
+ # 特殊鍵的顯示名稱 / Friendlier names for special keys
33
+ _SPECIAL_NAMES = {
34
+ "space": "Space", "enter": "Enter", "return": "Enter", "tab": "Tab",
35
+ "backspace": "Backspace", "delete": "Del", "esc": "Esc", "escape": "Esc",
36
+ "up": "↑", "down": "↓", "left": "←", "right": "→",
37
+ "page_up": "PgUp", "page_down": "PgDn", "home": "Home", "end": "End",
38
+ }
39
+
40
+
41
+ def format_key(name) -> str:
42
+ """
43
+ 把按鍵名稱轉成畫面上好讀的字(修飾鍵、方向鍵、功能鍵各有寫法)。
44
+ Turn a key name into something readable on screen.
45
+ """
46
+ text = str(name or "").strip().strip("'")
47
+ if not text:
48
+ return ""
49
+ lowered = text.lower()
50
+ if lowered in _MODIFIER_NAMES:
51
+ return _MODIFIER_NAMES[lowered]
52
+ if lowered in _SPECIAL_NAMES:
53
+ return _SPECIAL_NAMES[lowered]
54
+ if lowered.startswith("key."):
55
+ return format_key(text[4:])
56
+ if len(text) == 1:
57
+ return text.upper()
58
+ if lowered.startswith("f") and lowered[1:].isdigit():
59
+ return text.upper()
60
+ return text.replace("_", " ").title()
61
+
62
+
63
+ def format_combo(keys) -> str:
64
+ """把同時按著的按鍵組成 `Ctrl + Shift + S` 這種字串(去重、保留順序)。"""
65
+ parts: List[str] = []
66
+ for key in keys or ():
67
+ formatted = format_key(key)
68
+ if formatted and formatted not in parts:
69
+ parts.append(formatted)
70
+ return " + ".join(parts)
71
+
72
+
73
+ def visible_keys(entries, now: float, hold_seconds: float = DEFAULT_HOLD_SECONDS,
74
+ limit: int = MAX_KEYS_SHOWN) -> List[str]:
75
+ """
76
+ 從 (文字, 時間) 清單裡挑出還沒過期的按鍵,最多顯示 limit 個(最新在後)。
77
+ The still-fresh entries from a list of (text, timestamp) pairs.
78
+ """
79
+ fresh = [text for text, stamp in entries or () if now - stamp <= max(0.1, float(hold_seconds))]
80
+ return fresh[-max(1, int(limit)):]
81
+
82
+
83
+ class KeystrokeDisplayWidget(BaseWidget):
84
+ """
85
+ 按鍵顯示層:把最近按下的按鍵排成一列顯示,過幾秒自動淡出。點擊穿透。
86
+ 按鍵來源由外部推入(`push_keys`),因此不需要在這裡自己開監聽器。
87
+ """
88
+
89
+ REFRESH_MS = 100
90
+ PADDING = 12
91
+
92
+ def __init__(self, hold_seconds: float = DEFAULT_HOLD_SECONDS, font_size: int = 28,
93
+ color: str = "#ffffff", background: str = "#000000") -> None:
94
+ front_engine_logger.info(f"[KeystrokeDisplayWidget] Init | hold={hold_seconds}s")
95
+ super().__init__()
96
+ self.opacity = 1.0
97
+ self.hold_seconds = max(0.2, float(hold_seconds))
98
+ self.text_color = QColor(color) if QColor(color).isValid() else QColor("#ffffff")
99
+ self.background_color = QColor(background) if QColor(background).isValid() else QColor("#000000")
100
+ self.font_size = max(8, int(font_size))
101
+ self.entries: List[Tuple[str, float]] = []
102
+ self._now = time.monotonic
103
+ self._timer = QTimer(self)
104
+ self._timer.timeout.connect(self._expire)
105
+
106
+ def set_clock(self, clock) -> None:
107
+ """注入時間來源(測試用)。"""
108
+ self._now = clock
109
+
110
+ def start(self, interval_ms: int = REFRESH_MS) -> None:
111
+ """開始定期清掉過期的按鍵。"""
112
+ self._timer.start(max(30, int(interval_ms)))
113
+
114
+ def push_keys(self, keys) -> None:
115
+ """
116
+ 推入一次按鍵事件(單一鍵或組合鍵);空的輸入忽略。
117
+ Record one keypress (single key or combination); blanks are ignored.
118
+ """
119
+ text = format_combo(keys if isinstance(keys, (list, tuple, set)) else [keys])
120
+ if not text:
121
+ return
122
+ self.entries.append((text, self._now()))
123
+ self.entries = self.entries[-(MAX_KEYS_SHOWN * 2):]
124
+ self.update()
125
+
126
+ def current_text(self) -> str:
127
+ """目前該顯示的字串。"""
128
+ return " ".join(visible_keys(self.entries, self._now(), self.hold_seconds))
129
+
130
+ def _expire(self) -> None:
131
+ before = len(visible_keys(self.entries, self._now(), self.hold_seconds))
132
+ if before != len(self.entries):
133
+ self.entries = [
134
+ (text, stamp) for text, stamp in self.entries
135
+ if self._now() - stamp <= self.hold_seconds
136
+ ]
137
+ self.update()
138
+
139
+ def draw_content(self, painter: QPainter) -> None:
140
+ text = self.current_text()
141
+ if not text:
142
+ return
143
+ font = QFont(self.font().family(), self.font_size)
144
+ painter.setFont(font)
145
+ metrics = QFontMetrics(font)
146
+ width = metrics.horizontalAdvance(text) + self.PADDING * 2
147
+ height = metrics.height() + self.PADDING
148
+ box = QRect(max(0, (self.width() - width) // 2),
149
+ max(0, self.height() - height - self.PADDING), width, height)
150
+ background = QColor(self.background_color)
151
+ background.setAlpha(170)
152
+ painter.setPen(Qt.PenStyle.NoPen)
153
+ painter.setBrush(background)
154
+ painter.drawRoundedRect(box, 8, 8)
155
+ painter.setPen(self.text_color)
156
+ painter.drawText(box, int(Qt.AlignmentFlag.AlignCenter), text)
157
+
158
+ def closeEvent(self, event) -> None:
159
+ if self._timer.isActive():
160
+ self._timer.stop()
161
+ super().closeEvent(event)
@@ -0,0 +1,127 @@
1
+ """
2
+ 區域放大鏡:抓取游標周圍的一小塊畫面,放大後顯示在覆蓋層上,
3
+ 方便簡報時展示細節(ZoomIt 式)。點擊穿透,抓圖用 Qt 自己的螢幕擷取。
4
+
5
+ A region magnifier: grab a small area around the cursor and draw it enlarged,
6
+ for showing detail during a demo. Click-through, and the capture uses Qt's own
7
+ screen grab rather than any platform API.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Optional, Tuple
12
+
13
+ from PySide6.QtCore import QRect, QTimer, Qt
14
+ from PySide6.QtGui import QColor, QCursor, QGuiApplication, QPainter, QPixmap
15
+
16
+ from frontengine.show.base_widget import BaseWidget
17
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
18
+
19
+ MIN_ZOOM = 1.25
20
+ MAX_ZOOM = 8.0
21
+ DEFAULT_ZOOM = 2.0
22
+ DEFAULT_SIZE = 240
23
+
24
+
25
+ def clamp_zoom(value, fallback: float = DEFAULT_ZOOM) -> float:
26
+ """把放大倍率夾在 1.25x~8x(再高就只剩馬賽克)。"""
27
+ try:
28
+ return max(MIN_ZOOM, min(MAX_ZOOM, round(float(value), 2)))
29
+ except (TypeError, ValueError):
30
+ return fallback
31
+
32
+
33
+ def source_rect(center: Tuple[int, int], size: int, zoom: float,
34
+ bounds: Tuple[int, int, int, int]) -> QRect:
35
+ """
36
+ 算出要擷取的螢幕區域:以游標為中心、邊長為顯示尺寸除以倍率,並限制在螢幕內。
37
+ The screen area to grab: centred on the cursor, sized by the zoom factor,
38
+ and kept inside the screen.
39
+ """
40
+ side = max(8, int(int(size) / max(MIN_ZOOM, float(zoom))))
41
+ left, top, right, bottom = bounds
42
+ x = int(center[0]) - side // 2
43
+ y = int(center[1]) - side // 2
44
+ x = max(left, min(x, max(left, right - side)))
45
+ y = max(top, min(y, max(top, bottom - side)))
46
+ return QRect(x, y, side, side)
47
+
48
+
49
+ class MagnifierWidget(BaseWidget):
50
+ """
51
+ 放大鏡:定期擷取游標周圍畫面並放大顯示。擷取來源可注入,方便測試。
52
+ """
53
+
54
+ REFRESH_MS = 60
55
+
56
+ def __init__(self, zoom: float = DEFAULT_ZOOM, size: int = DEFAULT_SIZE) -> None:
57
+ front_engine_logger.info(f"[MagnifierWidget] Init | zoom={zoom}, size={size}")
58
+ super().__init__()
59
+ self.opacity = 1.0
60
+ self.zoom = clamp_zoom(zoom)
61
+ self.view_size = max(80, int(size))
62
+ self.captured: Optional[QPixmap] = None
63
+ self._cursor_provider = QCursor.pos
64
+ self._grabber = self._grab_screen
65
+ self._timer = QTimer(self)
66
+ self._timer.timeout.connect(self.refresh)
67
+
68
+ def set_cursor_provider(self, provider) -> None:
69
+ self._cursor_provider = provider
70
+
71
+ def set_grabber(self, grabber) -> None:
72
+ """注入擷取函式:接收 QRect,回傳 QPixmap(測試用)。"""
73
+ self._grabber = grabber
74
+
75
+ def set_zoom(self, zoom: Optional[float] = None, size: Optional[int] = None) -> None:
76
+ if zoom is not None:
77
+ self.zoom = clamp_zoom(zoom)
78
+ if size is not None:
79
+ self.view_size = max(80, int(size))
80
+ self.resize(self.view_size, self.view_size)
81
+ self.update()
82
+
83
+ def start_following(self, interval_ms: int = REFRESH_MS) -> None:
84
+ self.refresh()
85
+ self._timer.start(max(16, int(interval_ms)))
86
+
87
+ @staticmethod
88
+ def _grab_screen(rect: QRect) -> Optional[QPixmap]:
89
+ """用 Qt 擷取指定螢幕區域;沒有可用螢幕時回傳 None。"""
90
+ screen = QGuiApplication.primaryScreen()
91
+ if screen is None:
92
+ return None
93
+ return screen.grabWindow(0, rect.x(), rect.y(), rect.width(), rect.height())
94
+
95
+ def screen_bounds(self) -> Tuple[int, int, int, int]:
96
+ screen = self.screen() or QGuiApplication.primaryScreen()
97
+ if screen is None:
98
+ return (0, 0, 1920, 1080)
99
+ geometry = screen.geometry()
100
+ return (geometry.left(), geometry.top(), geometry.right(), geometry.bottom())
101
+
102
+ def refresh(self) -> None:
103
+ """擷取游標周圍的畫面 / Grab the area around the cursor."""
104
+ try:
105
+ position = self._cursor_provider()
106
+ rect = source_rect((position.x(), position.y()), self.view_size, self.zoom,
107
+ self.screen_bounds())
108
+ self.captured = self._grabber(rect)
109
+ except Exception as error: # pragma: no cover - capture boundary
110
+ front_engine_logger.debug(f"[MagnifierWidget] capture failed: {error!r}")
111
+ return
112
+ self.update()
113
+
114
+ def draw_content(self, painter: QPainter) -> None:
115
+ if self.captured is None or self.captured.isNull():
116
+ return
117
+ painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
118
+ target = QRect(0, 0, self.width(), self.height())
119
+ painter.drawPixmap(target, self.captured)
120
+ painter.setPen(QColor(255, 255, 255, 180))
121
+ painter.setBrush(Qt.BrushStyle.NoBrush)
122
+ painter.drawRect(target.adjusted(0, 0, -1, -1))
123
+
124
+ def closeEvent(self, event) -> None:
125
+ if self._timer.isActive():
126
+ self._timer.stop()
127
+ super().closeEvent(event)