frontengine 1.0.56__tar.gz → 1.0.57__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.
Files changed (254) hide show
  1. {frontengine-1.0.56 → frontengine-1.0.57}/PKG-INFO +1 -1
  2. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/base_widget.py +28 -3
  3. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/camera/camera_widget.py +10 -0
  4. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/canvas/whiteboard_widget.py +5 -0
  5. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/capture/region_capture.py +7 -0
  6. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/focus_shield/focus_shield_widget.py +16 -0
  7. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/gif/paint_gif.py +6 -0
  8. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/image/paint_image.py +7 -1
  9. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/load/load_someone_make_ui.py +34 -11
  10. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/measure/measure_widget.py +14 -2
  11. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/notes/sticky_note_widget.py +6 -0
  12. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/overlay_factory.py +36 -23
  13. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/particle/particle_ui.py +24 -2
  14. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/pet/desktop_pet.py +21 -3
  15. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/presentation/annotation_overlay.py +8 -0
  16. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/presentation/cursor_effects.py +8 -1
  17. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/presentation/keystroke_display.py +14 -6
  18. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/presentation/magnifier.py +17 -5
  19. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/scene/scene.py +19 -0
  20. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/sound_player/sound_effect.py +4 -1
  21. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/toast/toast_widget.py +27 -4
  22. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/video/video_player.py +5 -1
  23. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/system_tray/extend_system_tray.py +31 -38
  24. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/choose_file_dialog.py +14 -2
  25. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/hotkey_settings_dialog.py +8 -4
  26. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/reminder_dialog.py +37 -1
  27. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/remote_control_dialog.py +23 -3
  28. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/window_pin_dialog.py +16 -1
  29. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/main_ui.py +92 -26
  30. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/menu/settings_menu.py +30 -7
  31. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/control_center/control_center_ui.py +42 -16
  32. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/focus/focus_setting_ui.py +9 -9
  33. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/pet/pet_setting_ui.py +13 -8
  34. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/presentation/presentation_setting_ui.py +21 -21
  35. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_manager.py +67 -18
  36. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_setting_ui.py +1 -1
  37. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/screen_care/screen_care_setting_ui.py +17 -17
  38. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +12 -7
  39. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/text/text_setting_ui.py +34 -7
  40. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/tools/tools_setting_ui.py +24 -12
  41. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/wallpaper/wallpaper_setting_ui.py +5 -5
  42. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/web/web_setting_ui.py +12 -2
  43. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/widgets/widgets_setting_ui.py +5 -1
  44. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/user_setting/user_setting_file.py +10 -1
  45. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/audio_meter/loopback_capture.py +25 -3
  46. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/clipboard/clipboard_history.py +8 -1
  47. frontengine-1.0.57/frontengine/utils/critical_exit/critical_exit.py +97 -0
  48. frontengine-1.0.57/frontengine/utils/exception/exceptions.py +26 -0
  49. frontengine-1.0.57/frontengine/utils/json/json_file.py +61 -0
  50. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/json/json_repository.py +12 -1
  51. frontengine-1.0.57/frontengine/utils/logging/loggin_instance.py +80 -0
  52. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/midi/midi_input.py +10 -1
  53. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/english.py +4 -0
  54. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/france.py +4 -0
  55. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/germany.py +4 -0
  56. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/italy.py +4 -0
  57. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/russian.py +4 -0
  58. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/simplified_chinese.py +4 -0
  59. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/traditional_chinese.py +4 -0
  60. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/pet_chat/pet_chat_service.py +14 -0
  61. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/platform_info/platform_info.py +8 -1
  62. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/playlist/playlist.py +6 -1
  63. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/preset_schedule/preset_schedule_service.py +16 -1
  64. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/recording/gif_writer.py +46 -4
  65. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/reminder/reminder_service.py +19 -1
  66. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/remote/remote_server.py +16 -2
  67. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/screen_privacy/share_watch.py +15 -1
  68. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/text_source/text_source.py +5 -1
  69. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/weather/weather_service.py +21 -10
  70. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/window_pin/window_layout.py +14 -3
  71. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine.egg-info/PKG-INFO +1 -1
  72. {frontengine-1.0.56 → frontengine-1.0.57}/pyproject.toml +1 -1
  73. frontengine-1.0.56/frontengine/utils/critical_exit/critical_exit.py +0 -65
  74. frontengine-1.0.56/frontengine/utils/exception/exceptions.py +0 -18
  75. frontengine-1.0.56/frontengine/utils/json/json_file.py +0 -37
  76. frontengine-1.0.56/frontengine/utils/logging/loggin_instance.py +0 -46
  77. {frontengine-1.0.56 → frontengine-1.0.57}/LICENSE +0 -0
  78. {frontengine-1.0.56 → frontengine-1.0.57}/README.md +0 -0
  79. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/__init__.py +0 -0
  80. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/__main__.py +0 -0
  81. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/__init__.py +0 -0
  82. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/camera/__init__.py +0 -0
  83. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/canvas/__init__.py +0 -0
  84. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/capture/__init__.py +0 -0
  85. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/focus_shield/__init__.py +0 -0
  86. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/gif/__init__.py +0 -0
  87. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/image/__init__.py +0 -0
  88. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/load/__init__.py +0 -0
  89. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/measure/__init__.py +0 -0
  90. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/monitor/__init__.py +0 -0
  91. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/monitor/monitor_widget.py +0 -0
  92. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/notes/__init__.py +0 -0
  93. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/particle/__init__.py +0 -0
  94. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/pet/__init__.py +0 -0
  95. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/presentation/__init__.py +0 -0
  96. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/scene/__init__.py +0 -0
  97. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  98. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/scene/extend_graphic_view.py +0 -0
  99. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/screen_care/__init__.py +0 -0
  100. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/screen_care/color_vision_widget.py +0 -0
  101. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/screen_care/screen_filter.py +0 -0
  102. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/sound_player/__init__.py +0 -0
  103. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/sound_player/sound_player.py +0 -0
  104. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/spectrum/__init__.py +0 -0
  105. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/spectrum/spectrum_widget.py +0 -0
  106. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/text/__init__.py +0 -0
  107. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/text/draw_text.py +0 -0
  108. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/toast/__init__.py +0 -0
  109. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/video/__init__.py +0 -0
  110. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/wallpaper/__init__.py +0 -0
  111. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/wallpaper/wallpaper_widget.py +0 -0
  112. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/web/__init__.py +0 -0
  113. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/web/webview.py +0 -0
  114. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/show/window_helpers.py +0 -0
  115. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/system_tray/__init__.py +0 -0
  116. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/__init__.py +0 -0
  117. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/color/__init__.py +0 -0
  118. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/color/global_color.py +0 -0
  119. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/__init__.py +0 -0
  120. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/app_profile_dialog.py +0 -0
  121. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/clipboard_dialog.py +0 -0
  122. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/how_to_use_dialog.py +0 -0
  123. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/screen_privacy_dialog.py +0 -0
  124. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/screen_text_dialog.py +0 -0
  125. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/signage_dialog.py +0 -0
  126. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/smart_pause_dialog.py +0 -0
  127. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/dialog/usage_report_dialog.py +0 -0
  128. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/menu/__init__.py +0 -0
  129. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/menu/help_menu.py +0 -0
  130. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/menu/how_to_menu.py +0 -0
  131. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/menu/language_menu.py +0 -0
  132. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/menu/preset_menu.py +0 -0
  133. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/__init__.py +0 -0
  134. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/control_center/__init__.py +0 -0
  135. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/focus/__init__.py +0 -0
  136. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/gif/__init__.py +0 -0
  137. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  138. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/image/__init__.py +0 -0
  139. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  140. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/particle/__init__.py +0 -0
  141. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  142. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/pet/__init__.py +0 -0
  143. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/presentation/__init__.py +0 -0
  144. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  145. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  146. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  147. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  148. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  149. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  150. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  151. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  152. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  153. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  154. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/screen_care/__init__.py +0 -0
  155. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/sound_player/__init__.py +0 -0
  156. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/text/__init__.py +0 -0
  157. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/tools/__init__.py +0 -0
  158. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/utils.py +0 -0
  159. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/video/__init__.py +0 -0
  160. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  161. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/wallpaper/__init__.py +0 -0
  162. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/web/__init__.py +0 -0
  163. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/ui/page/widgets/__init__.py +0 -0
  164. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/user_setting/__init__.py +0 -0
  165. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/user_setting/preset_repository.py +0 -0
  166. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/user_setting/scene_setting.py +0 -0
  167. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/__init__.py +0 -0
  168. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/app_profile/__init__.py +0 -0
  169. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/app_profile/app_profile_service.py +0 -0
  170. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/audio_meter/__init__.py +0 -0
  171. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  172. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/audio_meter/microphone_meter.py +0 -0
  173. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  174. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/audio_meter/spectrum_analyzer.py +0 -0
  175. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  176. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/autostart/__init__.py +0 -0
  177. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/autostart/autostart_service.py +0 -0
  178. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/break_reminder/__init__.py +0 -0
  179. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/break_reminder/break_reminder.py +0 -0
  180. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/browser/__init__.py +0 -0
  181. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/browser/browser.py +0 -0
  182. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/clipboard/__init__.py +0 -0
  183. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/clipboard/clipboard_watcher.py +0 -0
  184. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/color_vision/__init__.py +0 -0
  185. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/color_vision/color_vision.py +0 -0
  186. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/critical_exit/__init__.py +0 -0
  187. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  188. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  189. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/exception/__init__.py +0 -0
  190. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/exception/exception_tags.py +0 -0
  191. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/focus_shield/__init__.py +0 -0
  192. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/focus_shield/focus_shield.py +0 -0
  193. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/focus_timer/__init__.py +0 -0
  194. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/focus_timer/focus_timer.py +0 -0
  195. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/hotkey/__init__.py +0 -0
  196. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  197. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/input_watch/__init__.py +0 -0
  198. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/input_watch/input_watch_service.py +0 -0
  199. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/json/__init__.py +0 -0
  200. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/logging/__init__.py +0 -0
  201. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/measure/__init__.py +0 -0
  202. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/measure/measure.py +0 -0
  203. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/midi/__init__.py +0 -0
  204. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/__init__.py +0 -0
  205. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  206. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/multi_language/retranslate.py +0 -0
  207. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/now_playing/__init__.py +0 -0
  208. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/now_playing/now_playing.py +0 -0
  209. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/pet_chat/__init__.py +0 -0
  210. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/platform_info/__init__.py +0 -0
  211. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/playlist/__init__.py +0 -0
  212. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/plugins/__init__.py +0 -0
  213. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/plugins/plugin_loader.py +0 -0
  214. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/power_mode/__init__.py +0 -0
  215. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/power_mode/power_mode.py +0 -0
  216. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/preset_schedule/__init__.py +0 -0
  217. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/recording/__init__.py +0 -0
  218. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/recording/frame_recorder.py +0 -0
  219. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/redirect_manager/__init__.py +0 -0
  220. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  221. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/reminder/__init__.py +0 -0
  222. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/remote/__init__.py +0 -0
  223. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/screen_privacy/__init__.py +0 -0
  224. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/screen_privacy/capture_affinity.py +0 -0
  225. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/screen_text/__init__.py +0 -0
  226. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/screen_text/screen_text_service.py +0 -0
  227. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/signage/__init__.py +0 -0
  228. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/signage/signage_service.py +0 -0
  229. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/smart_pause/__init__.py +0 -0
  230. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/smart_pause/pause_rules.py +0 -0
  231. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  232. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/system_stats/__init__.py +0 -0
  233. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/system_stats/system_stats.py +0 -0
  234. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/text_source/__init__.py +0 -0
  235. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/theme_schedule/__init__.py +0 -0
  236. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  237. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/usage_tracking/__init__.py +0 -0
  238. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/usage_tracking/usage_service.py +0 -0
  239. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/usage_tracking/usage_tracker.py +0 -0
  240. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/virtual_camera/__init__.py +0 -0
  241. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/virtual_camera/camera_feed.py +0 -0
  242. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/virtual_camera/virtual_camera.py +0 -0
  243. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/weather/__init__.py +0 -0
  244. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/web_url.py +0 -0
  245. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/window_pin/__init__.py +0 -0
  246. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/window_pin/window_pin.py +0 -0
  247. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/workshop/__init__.py +0 -0
  248. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine/utils/workshop/workshop_content.py +0 -0
  249. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine.egg-info/SOURCES.txt +0 -0
  250. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine.egg-info/dependency_links.txt +0 -0
  251. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine.egg-info/entry_points.txt +0 -0
  252. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine.egg-info/requires.txt +0 -0
  253. {frontengine-1.0.56 → frontengine-1.0.57}/frontengine.egg-info/top_level.txt +0 -0
  254. {frontengine-1.0.56 → frontengine-1.0.57}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.56
3
+ Version: 1.0.57
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
@@ -26,6 +26,21 @@ class BaseWidget(QWidget):
26
26
  # Locked means click-through; unlocked overlays can be dragged into place.
27
27
  self.overlay_locked: bool = True
28
28
  self.overlay_lockable: bool = True
29
+ # 自己處理滑鼠的覆蓋層(畫筆、白板、框選、量尺、寵物)要把這個關掉。
30
+ # 不關的話,使用者一在上面拖曳,基底的「拖曳擺位」就把整個視窗一起搬走:
31
+ # 畫布滑出畫面,框選還會擷取到跟選取範圍不同的區域。
32
+ # Overlays that handle the mouse themselves - pen, whiteboard, region
33
+ # select, ruler, pet - switch this off. Otherwise dragging to draw also
34
+ # drags the window: the canvas slides off screen, and region capture
35
+ # grabs an area other than the one that was selected.
36
+ self.overlay_draggable: bool = True
37
+ # 位置記憶是以類別名稱為鍵的,也就是「同一類共用一份」。每個實例位置
38
+ # 各異的(便利貼、寵物)要關掉,否則會全部被拉到同一個位置與大小。
39
+ # The remembered geometry is keyed by class name, i.e. shared by every
40
+ # instance of that kind. Overlays whose instances each have their own
41
+ # place - sticky notes, the pet - switch it off, or they all snap to one
42
+ # position and size.
43
+ self.overlay_remembers_geometry: bool = True
29
44
  self.overlay_show_on_bottom: bool = False
30
45
  # 綠幕背景色:設了就以不透明色填滿背景,方便 OBS 去背
31
46
  # Chroma key colour: fills the background opaquely so OBS can key it out.
@@ -94,12 +109,13 @@ class BaseWidget(QWidget):
94
109
 
95
110
  # --- drag to position (only while unlocked) --------------------------
96
111
  def mousePressEvent(self, event) -> None:
97
- if not self.overlay_locked and event.button() == Qt.MouseButton.LeftButton:
112
+ if self.overlay_draggable and not self.overlay_locked \
113
+ and event.button() == Qt.MouseButton.LeftButton:
98
114
  self._drag_origin = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
99
115
  super().mousePressEvent(event)
100
116
 
101
117
  def mouseMoveEvent(self, event) -> None:
102
- if not self.overlay_locked and self._drag_origin is not None:
118
+ if self.overlay_draggable and not self.overlay_locked and self._drag_origin is not None:
103
119
  self.move(event.globalPosition().toPoint() - self._drag_origin)
104
120
  super().mouseMoveEvent(event)
105
121
 
@@ -111,6 +127,8 @@ class BaseWidget(QWidget):
111
127
 
112
128
  def remember_geometry(self) -> None:
113
129
  """記住目前位置與大小,之後同類覆蓋層會開在同一處。"""
130
+ if not self.overlay_remembers_geometry:
131
+ return
114
132
  geometry = self.geometry()
115
133
  front_engine_logger.info(
116
134
  f"{self.__class__.__name__} remember_geometry | {geometry.x()},{geometry.y()} "
@@ -129,12 +147,19 @@ class BaseWidget(QWidget):
129
147
 
130
148
  def restore_saved_geometry(self) -> None:
131
149
  """若這類覆蓋層有記住的位置就套用(使用者拖曳過才會有)。"""
150
+ if not self.overlay_remembers_geometry:
151
+ return
132
152
  saved = get_overlay_geometry(self.__class__.__name__)
133
153
  if not saved:
134
154
  return
135
155
  try:
136
156
  if self.isFullScreen():
137
- self.showNormal()
157
+ # 使用者這次明講要全螢幕(含「顯示在所有螢幕」),就別拿記住的
158
+ # 位置把它拉回視窗模式——那會讓每台螢幕的覆蓋層全擠到同一格。
159
+ # The caller explicitly asked for fullscreen (this is also how
160
+ # "show on all screens" presents each one). Dragging it back to a
161
+ # remembered rect would pile every screen's overlay onto one spot.
162
+ return
138
163
  self.setGeometry(*saved)
139
164
  except RuntimeError: # pragma: no cover - widget closed mid-callback
140
165
  return
@@ -138,6 +138,16 @@ class CameraWidget(BaseWidget):
138
138
  self._camera.stop()
139
139
  except RuntimeError: # pragma: no cover - already torn down
140
140
  pass
141
+ # sink 的 parent 是這個 widget,所以只把 Python 參考設成 None 不會銷毀它。
142
+ # 每次重新 start()(例如換攝影機)都會多留一個還連著 _on_frame 的孤兒。
143
+ # The sink is parented to this widget, so dropping the Python reference
144
+ # does not destroy it: every restart - switching camera, say - leaves
145
+ # another orphan still connected to _on_frame.
146
+ if self._sink is not None:
147
+ try:
148
+ self._sink.deleteLater()
149
+ except RuntimeError: # pragma: no cover - already torn down
150
+ pass
141
151
  self._camera = None
142
152
  self._session = None
143
153
  self._sink = None
@@ -79,6 +79,11 @@ class WhiteboardWidget(BaseWidget):
79
79
  self.pen_width = max(1, int(width))
80
80
  self.opacity = 1.0
81
81
  self.overlay_lockable = False
82
+ # 白板自己處理拖曳(畫線與平移),基底的拖曳擺位會把整塊畫布搬走
83
+ # The whiteboard handles its own dragging - strokes and panning - and the
84
+ # base class's drag-to-position would carry the whole canvas off with it.
85
+ self.overlay_draggable = False
86
+ self.overlay_remembers_geometry = False
82
87
  self.strokes: List[Dict[str, Any]] = []
83
88
  self.zoom = 1.0
84
89
  self.offset = [0.0, 0.0]
@@ -65,6 +65,13 @@ class RegionCaptureWidget(BaseWidget):
65
65
  super().__init__()
66
66
  self.opacity = 1.0
67
67
  self.overlay_lockable = False
68
+ # 框選的拖曳就是選取範圍本身。讓基底也跟著搬視窗的話,事件座標會跟著
69
+ # 位移,最後擷取到的不是使用者框起來的那一塊。
70
+ # Here the drag is the selection. Letting the base class move the window
71
+ # too shifts the event coordinates, so what gets grabbed is not the area
72
+ # the user framed.
73
+ self.overlay_draggable = False
74
+ self.overlay_remembers_geometry = False
68
75
  self.origin: Optional[QPoint] = None
69
76
  self.current: Optional[QPoint] = None
70
77
  self.captured: Optional[QPixmap] = None
@@ -84,12 +84,28 @@ class DimBackgroundWidget(BaseWidget):
84
84
  geometry = self.geometry()
85
85
  return (geometry.x(), geometry.y(), geometry.width(), geometry.height())
86
86
 
87
+ def _to_logical(self, rect: Optional[Tuple[int, int, int, int]]):
88
+ """
89
+ 把 Win32 的視窗矩形換算成 Qt 的邏輯座標。GetWindowRect 給的是實體像素,
90
+ screen_rect() 給的是邏輯像素,顯示縮放不是 100% 時兩者對不起來:在 150%
91
+ 的螢幕上,最大化的視窗會算成比整個螢幕還大,結果一塊都不壓暗。
92
+ Convert a Win32 window rect into Qt's logical coordinates. GetWindowRect
93
+ reports physical pixels while screen_rect() is logical, and the two
94
+ disagree at any scaling other than 100%: on a 150% display a maximised
95
+ window measures larger than the whole screen, so nothing gets dimmed.
96
+ """
97
+ if rect is None:
98
+ return None
99
+ ratio = self.devicePixelRatio() or 1.0
100
+ return tuple(int(round(value / ratio)) for value in rect)
101
+
87
102
  def refresh(self) -> None:
88
103
  """重新計算要壓暗的區塊。"""
89
104
  try:
90
105
  active = self._window_provider()
91
106
  except Exception: # pragma: no cover - defensive around providers
92
107
  active = None
108
+ active = self._to_logical(active)
93
109
  rects = surrounding_rects(self.screen_rect(), active)
94
110
  origin_x, origin_y = self.x(), self.y()
95
111
  # 換算成本視窗的座標系 / Convert to this widget's own coordinates
@@ -39,6 +39,12 @@ class GifWidget(BaseWidget):
39
39
  self.speed = speed
40
40
  self.movie.setSpeed(self.speed)
41
41
 
42
+ def closeEvent(self, event) -> None:
43
+ # 關掉之後還在解碼就是白燒 CPU,畫面早就不見了
44
+ # A movie still decoding after the overlay is gone is pure wasted CPU.
45
+ self.movie.stop()
46
+ super().closeEvent(event)
47
+
42
48
  def draw_content(self, painter: QPainter) -> None:
43
49
  current_gif_frame = self.movie.currentPixmap()
44
50
  painter.drawPixmap(
@@ -18,10 +18,14 @@ class ImageWidget(BaseWidget):
18
18
  def __init__(self, image_path: str, draw_location_x: int = 0, draw_location_y: int = 0):
19
19
  super().__init__(draw_location_x, draw_location_y)
20
20
  self.image_path: Path = Path(image_path)
21
+ # 先給一張空圖,路徑失效時 paintEvent 才不會撞上未定義的屬性
22
+ # Start from an empty image so a bad path cannot leave paintEvent
23
+ # facing an attribute that was never assigned.
24
+ self.image: QImage = QImage()
21
25
 
22
26
  if self.image_path.exists() and self.image_path.is_file():
23
27
  front_engine_logger.info(f"Loading image file: {self.image_path}")
24
- self.image: QImage = QImage(str(self.image_path))
28
+ self.image = QImage(str(self.image_path))
25
29
  self.resize(self.image.size())
26
30
  else:
27
31
  message_box: QMessageBox = QMessageBox(self)
@@ -45,6 +49,8 @@ class ImageWidget(BaseWidget):
45
49
  return True
46
50
 
47
51
  def draw_content(self, painter: QPainter) -> None:
52
+ if self.image.isNull():
53
+ return
48
54
  painter.drawImage(
49
55
  QRect(self.draw_location_x, self.draw_location_y, self.width(), self.height()),
50
56
  self.image
@@ -1,12 +1,30 @@
1
1
  from typing import Optional, List
2
2
 
3
- from PySide6.QtGui import QScreen
3
+ from PySide6.QtCore import Qt
4
+ from PySide6.QtGui import QGuiApplication
4
5
  from PySide6.QtUiTools import QUiLoader
5
6
  from PySide6.QtWidgets import QWidget
6
7
 
7
8
  from frontengine.utils.exception.exceptions import FrontEngineLoadUIException
8
9
  from frontengine.utils.logging.loggin_instance import front_engine_logger
9
10
 
11
+ # 載入的視窗沒有 parent,Python 這邊不留參考就會立刻被回收,視窗根本不會出現。
12
+ # A loaded window has no parent, so without a reference on the Python side it is
13
+ # collected the moment this function returns and never appears at all.
14
+ _loaded_ui_widgets: List[QWidget] = []
15
+
16
+
17
+ def _forget_closed_ui() -> None:
18
+ """把已被 Qt 銷毀的視窗從清單剔除,免得一直累積。"""
19
+ alive: List[QWidget] = []
20
+ for widget in _loaded_ui_widgets:
21
+ try:
22
+ widget.isVisible()
23
+ except RuntimeError: # 底層 C++ 物件已消滅 / underlying C++ object is gone
24
+ continue
25
+ alive.append(widget)
26
+ _loaded_ui_widgets[:] = alive
27
+
10
28
 
11
29
  def load_extend_ui_file(ui_path: str, show_all_screen: bool = False) -> None:
12
30
  """
@@ -19,19 +37,24 @@ def load_extend_ui_file(ui_path: str, show_all_screen: bool = False) -> None:
19
37
  front_engine_logger.info(
20
38
  f"[load_extend_ui_file] ui_path={ui_path}, show_all_screen={show_all_screen}"
21
39
  )
22
-
23
- ui: QWidget = load_ui_file(ui_path)
40
+ _forget_closed_ui()
24
41
 
25
42
  if show_all_screen:
26
- ui.showFullScreen()
43
+ # 一個 widget 不可能同時待在多台螢幕上,每台各載入一份
44
+ # One widget cannot sit on several screens at once, so load one per screen.
45
+ for screen in QGuiApplication.screens():
46
+ _present_ui(load_ui_file(ui_path), screen.availableGeometry().topLeft())
27
47
  else:
28
- # 取得所有螢幕並在每個螢幕上顯示 UI
29
- # Get all monitors and display UI on each
30
- monitors: List[QScreen] = QScreen.virtualSiblings(ui.screen())
31
- for screen in monitors:
32
- monitor_geometry = screen.availableGeometry()
33
- ui.move(monitor_geometry.left(), monitor_geometry.top())
34
- ui.showFullScreen()
48
+ _present_ui(load_ui_file(ui_path), None)
49
+
50
+
51
+ def _present_ui(ui: QWidget, top_left) -> None:
52
+ """全螢幕顯示一個載入好的視窗,並保留參考直到它被關閉。"""
53
+ ui.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
54
+ if top_left is not None:
55
+ ui.move(top_left)
56
+ ui.showFullScreen()
57
+ _loaded_ui_widgets.append(ui)
35
58
 
36
59
 
37
60
  def load_ui_file(ui_path: str) -> QWidget:
@@ -67,6 +67,10 @@ class MeasureWidget(BaseWidget):
67
67
  self.color_format = color_format
68
68
  self.opacity = 1.0
69
69
  self.overlay_lockable = False
70
+ # 量測靠的是螢幕座標,覆蓋層一被拖走就量錯
71
+ # Measuring is in screen coordinates; dragging the overlay falsifies it.
72
+ self.overlay_draggable = False
73
+ self.overlay_remembers_geometry = False
70
74
  self.points: List[QPoint] = []
71
75
  self.cursor_point = QPoint(0, 0)
72
76
  self.picked_color: Optional[QColor] = None
@@ -138,13 +142,21 @@ class MeasureWidget(BaseWidget):
138
142
  self.readout = self.color_text(color)
139
143
  self.update()
140
144
  return self.readout
141
- self.points.append(QPoint(point))
142
145
  needed = 2 if self.mode == MODE_RULER else 3
146
+ if len(self.points) >= needed:
147
+ # 上一次已經量完了,這一下是新一次量測的第一點。先前的寫法是把新的
148
+ # 點加進去、拿舊的點算完再截掉,等於每一次新點擊都重算並重新複製
149
+ # 同一個舊結果——量尺變成只能用一次。
150
+ # The previous measurement is finished, so this click starts the next
151
+ # one. The old code appended the new point, measured the *old* ones
152
+ # and truncated the new one away, so every later click recomputed and
153
+ # re-copied the same stale result: the ruler worked exactly once.
154
+ self.points.clear()
155
+ self.points.append(QPoint(point))
143
156
  if len(self.points) < needed:
144
157
  self.update()
145
158
  return None
146
159
  self.readout = self._finish_measurement()
147
- self.points = self.points[:needed]
148
160
  self.update()
149
161
  return self.readout
150
162
 
@@ -91,6 +91,12 @@ class StickyNoteWidget(BaseWidget):
91
91
  # 要打字就不能點擊穿透,也就不提供鎖定
92
92
  # Typing needs the mouse and keyboard, so this one is never lockable.
93
93
  self.overlay_lockable = False
94
+ # 便利貼可以拖,但每張各有各的位置,所以不用「同類共用一份」的位置記憶。
95
+ # 每張的位置與大小是由 note_state() 各自存的。
96
+ # Notes are draggable, but each has its own place, so the class-wide
97
+ # geometry memory does not apply: note_state() persists each note's own
98
+ # position and size.
99
+ self.overlay_remembers_geometry = False
94
100
  self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, False)
95
101
 
96
102
  self.text_edit = QTextEdit(self)
@@ -11,7 +11,14 @@ from frontengine.show.video.video_player import VideoWidget
11
11
  from frontengine.show.web.webview import WebWidget
12
12
 
13
13
 
14
- def _normalize_opacity(value: Optional[Any], default: float = 0.2) -> float:
14
+ def _normalize_percent(value: Optional[Any], default: float) -> float:
15
+ """
16
+ 場景 JSON 裡的百分比欄位(不透明度、音量、播放速率)轉成 0.0-1.0。
17
+ 沒填就用 default——default 已經是最終比例,不要再除以 100。
18
+ Turn a percentage field from the scene JSON (opacity, volume, play rate)
19
+ into a 0.0-1.0 ratio. A missing field falls back to `default`, which is
20
+ already the final ratio and must not be divided again.
21
+ """
15
22
  try:
16
23
  return float(value) / 100 if value is not None else default
17
24
  except (ValueError, TypeError):
@@ -25,11 +32,18 @@ def _normalize_int(value: Optional[Any], default: int) -> int:
25
32
  return default
26
33
 
27
34
 
28
- def _normalize_float(value: Optional[Any], default: float) -> float:
29
- try:
30
- return float(value) if value is not None else default
31
- except (ValueError, TypeError):
32
- return default
35
+ def _require(setting_dict: Dict[str, Any], key: str, kind: str) -> str:
36
+ """
37
+ 取出必要欄位。場景 JSON 是使用者自己編輯/外部提供的,缺欄位就明講,
38
+ 不要把 None 一路送進 Path() 或 QWebEngineView.load() 才炸在深處。
39
+ Fetch a required field. Scene JSON is hand-edited or third-party, so say
40
+ what is missing here instead of letting None reach Path() or
41
+ QWebEngineView.load() and blow up somewhere deeper.
42
+ """
43
+ value = setting_dict.get(key)
44
+ if not isinstance(value, str) or not value.strip():
45
+ raise ValueError(f"Scene entry of type '{kind}' is missing a valid '{key}'")
46
+ return value
33
47
 
34
48
 
35
49
  class OverlayFactory(ABC):
@@ -46,31 +60,30 @@ class OverlayFactory(ABC):
46
60
 
47
61
  class ImageOverlayFactory(OverlayFactory):
48
62
  def create(self, setting_dict: Dict[str, Any]) -> QWidget:
49
- widget = ImageWidget(setting_dict.get("file_path"))
50
- widget.set_ui_variable(_normalize_opacity(setting_dict.get("opacity")))
63
+ widget = ImageWidget(_require(setting_dict, "file_path", "image"))
64
+ widget.set_ui_variable(_normalize_percent(setting_dict.get("opacity"), 0.2))
51
65
  return widget
52
66
 
53
67
 
54
68
  class GifOverlayFactory(OverlayFactory):
55
69
  def create(self, setting_dict: Dict[str, Any]) -> QWidget:
56
- widget = GifWidget(setting_dict.get("file_path"))
70
+ widget = GifWidget(_require(setting_dict, "file_path", "gif"))
57
71
  widget.set_gif_variable(_normalize_int(setting_dict.get("speed"), 100))
58
- widget.set_ui_variable(_normalize_opacity(setting_dict.get("opacity")))
72
+ widget.set_ui_variable(_normalize_percent(setting_dict.get("opacity"), 0.2))
59
73
  return widget
60
74
 
61
75
 
62
76
  class SoundOverlayFactory(OverlayFactory):
63
77
  def create(self, setting_dict: Dict[str, Any]) -> QWidget:
64
- widget = SoundPlayer(setting_dict.get("file_path"))
65
- volume = _normalize_float(setting_dict.get("volume"), 1.0) / 100
66
- widget.set_player_variable(volume)
78
+ widget = SoundPlayer(_require(setting_dict, "file_path", "sound"))
79
+ widget.set_player_variable(_normalize_percent(setting_dict.get("volume"), 1.0))
67
80
  return widget
68
81
 
69
82
 
70
83
  class TextOverlayFactory(OverlayFactory):
71
84
  def create(self, setting_dict: Dict[str, Any]) -> QWidget:
72
- widget = TextWidget(setting_dict.get("text"))
73
- widget.set_ui_variable(_normalize_opacity(setting_dict.get("opacity")))
85
+ widget = TextWidget(_require(setting_dict, "text", "text"))
86
+ widget.set_ui_variable(_normalize_percent(setting_dict.get("opacity"), 0.2))
74
87
  widget.set_font_variable(_normalize_int(setting_dict.get("font_size"), 100))
75
88
  widget.set_alignment(setting_dict.get("alignment", "Center"))
76
89
  return widget
@@ -78,19 +91,19 @@ class TextOverlayFactory(OverlayFactory):
78
91
 
79
92
  class VideoOverlayFactory(OverlayFactory):
80
93
  def create(self, setting_dict: Dict[str, Any]) -> QWidget:
81
- widget = VideoWidget(setting_dict.get("file_path"))
82
- opacity = _normalize_opacity(setting_dict.get("opacity"))
83
- volume = _normalize_float(setting_dict.get("volume"), 1.0) / 100
84
- play_rate = _normalize_float(setting_dict.get("play_rate"), 1.0) / 100
85
- widget.set_ui_variable(opacity)
86
- widget.set_player_variable(play_rate, volume)
94
+ widget = VideoWidget(_require(setting_dict, "file_path", "video"))
95
+ widget.set_ui_variable(_normalize_percent(setting_dict.get("opacity"), 0.2))
96
+ widget.set_player_variable(
97
+ _normalize_percent(setting_dict.get("play_rate"), 1.0),
98
+ _normalize_percent(setting_dict.get("volume"), 1.0),
99
+ )
87
100
  return widget
88
101
 
89
102
 
90
103
  class WebOverlayFactory(OverlayFactory):
91
104
  def create(self, setting_dict: Dict[str, Any]) -> QWidget:
92
- widget = WebWidget(setting_dict.get("url"))
93
- widget.set_ui_variable(_normalize_opacity(setting_dict.get("opacity")))
105
+ widget = WebWidget(_require(setting_dict, "url", "web"))
106
+ widget.set_ui_variable(_normalize_percent(setting_dict.get("opacity"), 0.2))
94
107
  return widget
95
108
 
96
109
 
@@ -15,7 +15,7 @@ from OpenGL.GL import (
15
15
  glTexCoord2f, glTexImage2D, glTexParameteri, glTranslatef, glVertex2f, glViewport,
16
16
  )
17
17
  from PySide6.QtCore import Qt, QTimer
18
- from PySide6.QtGui import QSurfaceFormat, QPixmap
18
+ from PySide6.QtGui import QImage, QSurfaceFormat, QPixmap
19
19
  from PySide6.QtOpenGLWidgets import QOpenGLWidget
20
20
 
21
21
  from frontengine.show.window_helpers import apply_overlay_window_flags, load_overlay_icon
@@ -48,6 +48,7 @@ class ParticleOpenGLWidget(QOpenGLWidget):
48
48
 
49
49
  self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
50
50
  self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
51
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
51
52
  self.setAutoFillBackground(False)
52
53
  load_overlay_icon(self)
53
54
 
@@ -61,7 +62,13 @@ class ParticleOpenGLWidget(QOpenGLWidget):
61
62
  Qt.AspectRatioMode.KeepAspectRatio,
62
63
  Qt.TransformationMode.SmoothTransformation
63
64
  )
64
- self.image = scaled_pixmap.toImage()
65
+ # QPixmap.toImage() 給的是 ARGB32(記憶體順序 BGRA),直接當成 GL_RGBA 上傳
66
+ # 會把紅藍對調。轉成 RGBA8888 才和下面的 glTexImage2D 對得起來,
67
+ # 順帶去掉預乘 alpha,符合 GL_SRC_ALPHA 混色的預期。
68
+ # toImage() hands back ARGB32, i.e. BGRA in memory; uploading that as
69
+ # GL_RGBA swaps red and blue. RGBA8888 matches the glTexImage2D call
70
+ # below and is un-premultiplied, which is what GL_SRC_ALPHA expects.
71
+ self.image = scaled_pixmap.toImage().convertToFormat(QImage.Format.Format_RGBA8888)
65
72
 
66
73
  self.particle_count = particle_count
67
74
  self.particle_direction = particle_direction
@@ -99,6 +106,11 @@ class ParticleOpenGLWidget(QOpenGLWidget):
99
106
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
100
107
  glEnable(GL_TEXTURE_2D)
101
108
 
109
+ if self.image.isNull():
110
+ # 沒有材質就別上傳 0x0 的貼圖,畫的時候直接跳過
111
+ # No texture: skip the 0x0 upload and let paintGL bail out.
112
+ return
113
+
102
114
  w, h = self.image.width(), self.image.height()
103
115
 
104
116
  self.texture_id = glGenTextures(1)
@@ -144,6 +156,9 @@ class ParticleOpenGLWidget(QOpenGLWidget):
144
156
  glClearColor(0, 0, 0, 0)
145
157
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
146
158
 
159
+ if self.texture_id is None:
160
+ return
161
+
147
162
  glBindTexture(GL_TEXTURE_2D, self.texture_id)
148
163
  glColor4f(1, 1, 1, self.opacity)
149
164
 
@@ -175,3 +190,10 @@ class ParticleOpenGLWidget(QOpenGLWidget):
175
190
 
176
191
  def set_ui_window_flag(self, show_on_bottom: bool = False) -> None:
177
192
  apply_overlay_window_flags(self, show_on_bottom=show_on_bottom)
193
+
194
+ def closeEvent(self, event) -> None:
195
+ # 關掉之後還在跑 60Hz 更新上萬顆粒子,畫面卻早就沒了
196
+ # Without this the 60 Hz update keeps churning through every particle
197
+ # long after the overlay has gone from the screen.
198
+ self.timer.stop()
199
+ super().closeEvent(event)
@@ -1094,6 +1094,16 @@ class DesktopPetWidget(BaseWidget):
1094
1094
  )
1095
1095
  super().__init__()
1096
1096
  self.opacity = 1.0
1097
+ # 寵物自己處理拖曳,位置與大小也由 PetMotion 決定。交給基底記憶的話,
1098
+ # 拖過一次之後那個「位置+大小」會套到之後每一隻寵物身上——尺寸下拉選單
1099
+ # 就等於失效:新寵物被悄悄縮回舊尺寸,碰撞判定卻還用新的大小。
1100
+ # The pet drags itself and PetMotion owns its geometry. Letting the base
1101
+ # class remember it means one drag pins that position and size onto every
1102
+ # later pet - which silently disables the size combobox: a new pet is
1103
+ # resized back while its collision box still uses the size that was asked
1104
+ # for.
1105
+ self.overlay_draggable = False
1106
+ self.overlay_remembers_geometry = False
1097
1107
  # 基礎尺寸依已存等級放大 / Base size grown by the persisted level.
1098
1108
  self._base_size: int = max(16, int(size))
1099
1109
  self._growth = PetGrowth(user_setting_dict.get("pet_affection", 0))
@@ -1189,7 +1199,9 @@ class DesktopPetWidget(BaseWidget):
1189
1199
  self.clone_action.triggered.connect(self.clone_requested.emit)
1190
1200
  self.menu.addAction(self.clone_action)
1191
1201
  self.feed_action = QAction(language_wrapper.language_word_dict.get("pet_feed", "Feed"), self)
1192
- self.feed_action.triggered.connect(self.feed)
1202
+ # triggered 會送出 checked(bool),直接接的話 feed 收到的 kind 是 False
1203
+ # triggered emits checked(bool), which would arrive as feed's `kind`.
1204
+ self.feed_action.triggered.connect(lambda: self.feed())
1193
1205
  self.menu.addAction(self.feed_action)
1194
1206
  self.status_action = QAction(language_wrapper.language_word_dict.get("pet_status", "Status"), self)
1195
1207
  self.status_action.triggered.connect(self.show_status)
@@ -1659,9 +1671,15 @@ class DesktopPetWidget(BaseWidget):
1659
1671
  else:
1660
1672
  self._pending_chat_reply = language_wrapper.language_word_dict.get(
1661
1673
  "pet_chat_failed", "I couldn't reach my thoughts just now.")
1662
- # 回覆可能在背景執行緒抵達,透過計時器回到 UI 執行緒再顯示
1674
+ # 回覆可能在背景執行緒抵達,透過計時器回到 UI 執行緒再顯示。
1675
+ # 一定要傳 context 物件(這裡是 self):兩個參數的 singleShot 會把計時器
1676
+ # 建在呼叫端那條執行緒上,而那條執行緒沒有事件迴圈,計時器永遠不會觸發
1677
+ # ——寵物就一直停在「讓我想想…」,回覆再也不會出現。
1663
1678
  # The reply may arrive off-thread; hop back to the UI thread to show it.
1664
- QTimer.singleShot(0, self._say_pending_chat_reply)
1679
+ # The context object (self) is essential: the two-argument singleShot
1680
+ # creates the timer on the calling thread, which has no event loop, so it
1681
+ # never fires - the pet stays on "let me think..." and the answer is lost.
1682
+ QTimer.singleShot(0, self, self._say_pending_chat_reply)
1665
1683
 
1666
1684
  def _say_pending_chat_reply(self) -> None:
1667
1685
  if self._pending_chat_reply:
@@ -67,6 +67,14 @@ class AnnotationOverlay(BaseWidget):
67
67
  front_engine_logger.info(f"[AnnotationOverlay] Init | tool={tool}, color={color}, width={width}")
68
68
  super().__init__()
69
69
  self.opacity = 1.0
70
+ # 這一層的滑鼠是拿來畫畫的:不能被基底拖走,也不能被「鎖定覆蓋層」
71
+ # 變成點擊穿透——那會讓畫筆整個失效,而畫好的線還留在畫面上。
72
+ # The mouse here is the pen: the base class must not drag the window with
73
+ # it, and "lock overlays" must not make it click-through - that kills the
74
+ # pen outright while the strokes stay frozen on screen.
75
+ self.overlay_lockable = False
76
+ self.overlay_draggable = False
77
+ self.overlay_remembers_geometry = False
70
78
  self.tool = tool if tool in TOOLS else TOOL_PEN
71
79
  self.pen_color = color
72
80
  self.pen_width = clamp_width(width)
@@ -111,7 +111,14 @@ class CursorEffectWidget(BaseWidget):
111
111
 
112
112
  def add_ripple(self, point: Optional[QPoint] = None) -> None:
113
113
  """在指定位置(預設為目前游標)加一圈點擊漣漪。"""
114
- self.ripples.append({"point": QPoint(point or self.cursor_point), "step": 0})
114
+ # QPoint 的真值判斷是 isNull(),所以 bool(QPoint(0, 0)) 是 False:
115
+ # 用 `point or ...` 的話,剛好點在覆蓋層左上角的那一下會被當成沒給座標,
116
+ # 漣漪畫到上一次輪詢到的游標位置去。
117
+ # QPoint's truthiness is isNull(), so bool(QPoint(0, 0)) is False: with
118
+ # `point or ...` a click exactly on the overlay's top-left corner reads as
119
+ # "no point given" and the ripple lands on the last polled cursor spot.
120
+ origin = self.cursor_point if point is None else point
121
+ self.ripples.append({"point": QPoint(origin), "step": 0})
115
122
  self.update()
116
123
 
117
124
  def tick(self) -> None:
@@ -128,12 +128,20 @@ class KeystrokeDisplayWidget(BaseWidget):
128
128
  return " ".join(visible_keys(self.entries, self._now(), self.hold_seconds))
129
129
 
130
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
- ]
131
+ # 拿還沒過期的「全部」數量來比,不要用 visible_keys——它只回傳最多
132
+ # MAX_KEYS_SHOWN 筆,一旦按超過那個數量,這個比較就永遠不相等,
133
+ # 每 100ms 都會重建清單並重畫一次,其實什麼都沒過期。
134
+ # Compare against the full count of unexpired entries, not visible_keys:
135
+ # that returns at most MAX_KEYS_SHOWN, so past that many keys the test is
136
+ # never equal and every 100 ms tick rebuilds the list and repaints with
137
+ # nothing actually having expired.
138
+ now = self._now()
139
+ fresh = [
140
+ (text, stamp) for text, stamp in self.entries
141
+ if now - stamp <= self.hold_seconds
142
+ ]
143
+ if len(fresh) != len(self.entries):
144
+ self.entries = fresh
137
145
  self.update()
138
146
 
139
147
  def draw_content(self, painter: QPainter) -> None:
@@ -86,14 +86,26 @@ class MagnifierWidget(BaseWidget):
86
86
 
87
87
  @staticmethod
88
88
  def _grab_screen(rect: QRect) -> Optional[QPixmap]:
89
- """用 Qt 擷取指定螢幕區域;沒有可用螢幕時回傳 None。"""
90
- screen = QGuiApplication.primaryScreen()
89
+ """
90
+ 擷取指定的螢幕區域。要用「那塊區域所在的那台螢幕」去擷取,不是主螢幕:
91
+ 放大鏡可以被指定開在副螢幕上,寫死主螢幕的話,游標移到另一台時擷取到的
92
+ 是完全不相干的位置。
93
+ Grab a screen region using the screen that region is actually on, not the
94
+ primary one: the magnifier can be placed on a secondary display, and
95
+ hard-coding the primary grabs somewhere else entirely once the cursor
96
+ moves across.
97
+ """
98
+ screen = QGuiApplication.screenAt(rect.center()) or QGuiApplication.primaryScreen()
91
99
  if screen is None:
92
100
  return None
93
101
  return screen.grabWindow(0, rect.x(), rect.y(), rect.width(), rect.height())
94
102
 
95
- def screen_bounds(self) -> Tuple[int, int, int, int]:
96
- screen = self.screen() or QGuiApplication.primaryScreen()
103
+ def screen_bounds(self, position=None) -> Tuple[int, int, int, int]:
104
+ """游標所在螢幕的範圍(沒給座標就用放大鏡自己那一台)。"""
105
+ screen = None
106
+ if position is not None:
107
+ screen = QGuiApplication.screenAt(position)
108
+ screen = screen or self.screen() or QGuiApplication.primaryScreen()
97
109
  if screen is None:
98
110
  return (0, 0, 1920, 1080)
99
111
  geometry = screen.geometry()
@@ -104,7 +116,7 @@ class MagnifierWidget(BaseWidget):
104
116
  try:
105
117
  position = self._cursor_provider()
106
118
  rect = source_rect((position.x(), position.y()), self.view_size, self.zoom,
107
- self.screen_bounds())
119
+ self.screen_bounds(position))
108
120
  self.captured = self._grabber(rect)
109
121
  except Exception as error: # pragma: no cover - capture boundary
110
122
  front_engine_logger.debug(f"[MagnifierWidget] capture failed: {error!r}")