frontengine 1.0.28__tar.gz → 1.0.30__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 (148) hide show
  1. {frontengine-1.0.28 → frontengine-1.0.30}/PKG-INFO +1 -1
  2. frontengine-1.0.30/frontengine/show/base_widget.py +125 -0
  3. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/pet/desktop_pet.py +77 -14
  4. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/text/draw_text.py +40 -0
  5. frontengine-1.0.30/frontengine/show/window_helpers.py +68 -0
  6. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/hotkey_settings_dialog.py +2 -0
  7. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/main_ui.py +4 -0
  8. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/control_center/control_center_ui.py +67 -2
  9. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/text/text_setting_ui.py +81 -1
  10. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/web/web_setting_ui.py +114 -2
  11. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/user_setting_file.py +34 -1
  12. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/english.py +29 -0
  13. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/traditional_chinese.py +29 -0
  14. frontengine-1.0.30/frontengine/utils/system_stats/system_stats.py +309 -0
  15. frontengine-1.0.30/frontengine/utils/text_source/text_source.py +186 -0
  16. frontengine-1.0.30/frontengine/utils/theme_schedule/__init__.py +0 -0
  17. frontengine-1.0.30/frontengine/utils/weather/__init__.py +0 -0
  18. frontengine-1.0.30/frontengine/utils/weather/weather_service.py +184 -0
  19. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/web_url.py +26 -0
  20. frontengine-1.0.30/frontengine/worker/__init__.py +0 -0
  21. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/PKG-INFO +1 -1
  22. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/SOURCES.txt +6 -0
  23. {frontengine-1.0.28 → frontengine-1.0.30}/pyproject.toml +1 -1
  24. frontengine-1.0.28/frontengine/show/base_widget.py +0 -47
  25. frontengine-1.0.28/frontengine/show/window_helpers.py +0 -40
  26. {frontengine-1.0.28 → frontengine-1.0.30}/LICENSE +0 -0
  27. {frontengine-1.0.28 → frontengine-1.0.30}/README.md +0 -0
  28. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/__init__.py +0 -0
  29. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/__main__.py +0 -0
  30. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/__init__.py +0 -0
  31. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/gif/__init__.py +0 -0
  32. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/gif/paint_gif.py +0 -0
  33. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/image/__init__.py +0 -0
  34. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/image/paint_image.py +0 -0
  35. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/load/__init__.py +0 -0
  36. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/load/load_someone_make_ui.py +0 -0
  37. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/overlay_factory.py +0 -0
  38. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/particle/__init__.py +0 -0
  39. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/particle/particle_ui.py +0 -0
  40. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/pet/__init__.py +0 -0
  41. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/__init__.py +0 -0
  42. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  43. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/extend_graphic_view.py +0 -0
  44. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/scene.py +0 -0
  45. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/sound_player/__init__.py +0 -0
  46. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/sound_player/sound_effect.py +0 -0
  47. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/sound_player/sound_player.py +0 -0
  48. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/text/__init__.py +0 -0
  49. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/video/__init__.py +0 -0
  50. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/video/video_player.py +0 -0
  51. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/web/__init__.py +0 -0
  52. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/web/webview.py +0 -0
  53. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/system_tray/__init__.py +0 -0
  54. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/system_tray/extend_system_tray.py +0 -0
  55. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/__init__.py +0 -0
  56. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/color/__init__.py +0 -0
  57. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/color/global_color.py +0 -0
  58. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/__init__.py +0 -0
  59. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  60. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  61. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/__init__.py +0 -0
  62. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/help_menu.py +0 -0
  63. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/how_to_menu.py +0 -0
  64. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/language_menu.py +0 -0
  65. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/preset_menu.py +0 -0
  66. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/settings_menu.py +0 -0
  67. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/__init__.py +0 -0
  68. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/control_center/__init__.py +0 -0
  69. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/gif/__init__.py +0 -0
  70. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  71. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/image/__init__.py +0 -0
  72. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  73. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/particle/__init__.py +0 -0
  74. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  75. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/pet/__init__.py +0 -0
  76. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
  77. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  78. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  79. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  80. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  81. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  82. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  83. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  84. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  85. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  86. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  87. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  88. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  89. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/sound_player/__init__.py +0 -0
  90. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  91. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/text/__init__.py +0 -0
  92. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/utils.py +0 -0
  93. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/video/__init__.py +0 -0
  94. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  95. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/web/__init__.py +0 -0
  96. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/__init__.py +0 -0
  97. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/preset_repository.py +0 -0
  98. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/scene_setting.py +0 -0
  99. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/__init__.py +0 -0
  100. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/__init__.py +0 -0
  101. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  102. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  103. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  104. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/browser/__init__.py +0 -0
  105. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/browser/browser.py +0 -0
  106. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/__init__.py +0 -0
  107. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  108. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  109. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  110. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/exception/__init__.py +0 -0
  111. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/exception/exception_tags.py +0 -0
  112. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/exception/exceptions.py +0 -0
  113. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/__init__.py +0 -0
  114. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/open/__init__.py +0 -0
  115. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/open/open_file.py +0 -0
  116. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/save/__init__.py +0 -0
  117. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/save/save_file.py +0 -0
  118. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/hotkey/__init__.py +0 -0
  119. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  120. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json/__init__.py +0 -0
  121. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json/json_file.py +0 -0
  122. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json/json_repository.py +0 -0
  123. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json_format/__init__.py +0 -0
  124. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json_format/json_process.py +0 -0
  125. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/logging/__init__.py +0 -0
  126. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/logging/loggin_instance.py +0 -0
  127. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/__init__.py +0 -0
  128. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/france.py +0 -0
  129. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/germany.py +0 -0
  130. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/italy.py +0 -0
  131. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  132. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/russian.py +0 -0
  133. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  134. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/preset_schedule/__init__.py +0 -0
  135. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  136. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/redirect_manager/__init__.py +0 -0
  137. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  138. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/smart_pause/__init__.py +0 -0
  139. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  140. {frontengine-1.0.28/frontengine/utils/theme_schedule → frontengine-1.0.30/frontengine/utils/system_stats}/__init__.py +0 -0
  141. {frontengine-1.0.28/frontengine/worker → frontengine-1.0.30/frontengine/utils/text_source}/__init__.py +0 -0
  142. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  143. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/worker/qthread_worker.py +0 -0
  144. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/dependency_links.txt +0 -0
  145. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/entry_points.txt +0 -0
  146. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/requires.txt +0 -0
  147. {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/top_level.txt +0 -0
  148. {frontengine-1.0.28 → frontengine-1.0.30}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.28
3
+ Version: 1.0.30
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,125 @@
1
+ from abc import abstractmethod
2
+ from typing import Optional
3
+
4
+ from PySide6.QtCore import Qt, QTimer
5
+ from PySide6.QtGui import QColor, QPainter
6
+ from PySide6.QtWidgets import QWidget
7
+
8
+ from frontengine.show.window_helpers import apply_overlay_window_flags, load_overlay_icon
9
+ from frontengine.user_setting.user_setting_file import get_overlay_geometry, save_overlay_geometry
10
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
11
+
12
+
13
+ class BaseWidget(QWidget):
14
+ """
15
+ BaseWidget: 提供共用 UI 屬性與事件處理的基底類別
16
+ BaseWidget: Base class providing shared UI attributes and event handling
17
+ """
18
+
19
+ def __init__(self, draw_location_x: int = 0, draw_location_y: int = 0):
20
+ super().__init__()
21
+ self.draw_location_x: int = draw_location_x
22
+ self.draw_location_y: int = draw_location_y
23
+ self.opacity: float = 0.2
24
+ # 鎖定=點擊穿透;解鎖後可用拖曳擺位(見 window_helpers.set_overlay_locked)
25
+ # Locked means click-through; unlocked overlays can be dragged into place.
26
+ self.overlay_locked: bool = True
27
+ self.overlay_lockable: bool = True
28
+ self.overlay_show_on_bottom: bool = False
29
+ # 綠幕背景色:設了就以不透明色填滿背景,方便 OBS 去背
30
+ # Chroma key colour: fills the background opaquely so OBS can key it out.
31
+ self.background_color: Optional[QColor] = None
32
+ self._drag_origin = None
33
+ self._geometry_restored = False
34
+
35
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
36
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
37
+ load_overlay_icon(self)
38
+
39
+ def set_ui_window_flag(self, show_on_bottom: bool = False) -> None:
40
+ front_engine_logger.info(f"{self.__class__.__name__} set_ui_window_flag | show_on_bottom: {show_on_bottom}")
41
+ apply_overlay_window_flags(self, show_on_bottom=show_on_bottom)
42
+
43
+ def set_ui_variable(self, opacity: float = 0.2) -> None:
44
+ front_engine_logger.info(f"{self.__class__.__name__} set_ui_variable | opacity: {opacity}")
45
+ self.opacity = opacity
46
+
47
+ def set_background_color(self, color) -> None:
48
+ """
49
+ 設定綠幕背景色(傳 None 取消)。背景以不透明色填滿,內容再依原本的
50
+ 不透明度疊上去,這樣 OBS 的 Chroma Key 才能乾淨去背。
51
+ Set the chroma key background colour (None clears it). The background is
52
+ filled opaquely and the content drawn over it at the usual opacity, so
53
+ OBS's chroma key filter can remove it cleanly.
54
+ """
55
+ if color is None:
56
+ self.background_color = None
57
+ else:
58
+ candidate = QColor(color)
59
+ self.background_color = candidate if candidate.isValid() else None
60
+ self.setAttribute(
61
+ Qt.WidgetAttribute.WA_TranslucentBackground, self.background_color is None)
62
+ self.update()
63
+
64
+ # --- drag to position (only while unlocked) --------------------------
65
+ def mousePressEvent(self, event) -> None:
66
+ if not self.overlay_locked and event.button() == Qt.MouseButton.LeftButton:
67
+ self._drag_origin = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
68
+ super().mousePressEvent(event)
69
+
70
+ def mouseMoveEvent(self, event) -> None:
71
+ if not self.overlay_locked and self._drag_origin is not None:
72
+ self.move(event.globalPosition().toPoint() - self._drag_origin)
73
+ super().mouseMoveEvent(event)
74
+
75
+ def mouseReleaseEvent(self, event) -> None:
76
+ if self._drag_origin is not None:
77
+ self._drag_origin = None
78
+ self.remember_geometry()
79
+ super().mouseReleaseEvent(event)
80
+
81
+ def remember_geometry(self) -> None:
82
+ """記住目前位置與大小,之後同類覆蓋層會開在同一處。"""
83
+ geometry = self.geometry()
84
+ front_engine_logger.info(
85
+ f"{self.__class__.__name__} remember_geometry | {geometry.x()},{geometry.y()} "
86
+ f"{geometry.width()}x{geometry.height()}"
87
+ )
88
+ save_overlay_geometry(
89
+ self.__class__.__name__, geometry.x(), geometry.y(), geometry.width(), geometry.height())
90
+
91
+ def showEvent(self, event) -> None:
92
+ super().showEvent(event)
93
+ if not self._geometry_restored:
94
+ self._geometry_restored = True
95
+ # 顯示流程結束後再套用,避免和 showFullScreen 打架
96
+ # Applied after the show completes so it does not fight showFullScreen.
97
+ QTimer.singleShot(0, self.restore_saved_geometry)
98
+
99
+ def restore_saved_geometry(self) -> None:
100
+ """若這類覆蓋層有記住的位置就套用(使用者拖曳過才會有)。"""
101
+ saved = get_overlay_geometry(self.__class__.__name__)
102
+ if not saved:
103
+ return
104
+ try:
105
+ if self.isFullScreen():
106
+ self.showNormal()
107
+ self.setGeometry(*saved)
108
+ except RuntimeError: # pragma: no cover - widget closed mid-callback
109
+ return
110
+
111
+ def paintEvent(self, event) -> None:
112
+ front_engine_logger.debug(f"{self.__class__.__name__} paintEvent | event: {event}")
113
+ painter = QPainter(self)
114
+ if self.background_color is not None:
115
+ painter.fillRect(self.rect(), self.background_color)
116
+ painter.save()
117
+ painter.setOpacity(self.opacity)
118
+ try:
119
+ self.draw_content(painter)
120
+ finally:
121
+ painter.restore()
122
+
123
+ @abstractmethod
124
+ def draw_content(self, painter: QPainter) -> None:
125
+ """Subclass hook that renders the widget content onto the shared painter."""
@@ -403,14 +403,28 @@ class PetTagGame:
403
403
  return self._roles(positions, prey)
404
404
 
405
405
  def _roles(self, positions: dict, prey) -> dict:
406
- """依目前的鬼與獵物組出每個人的角色與要走向的 x。"""
407
- it_x, _it_y = positions[self.it]
408
- roles = {self.it: (TAG_CHASE, positions[prey][0] if prey is not None else it_x)}
409
- for participant, (x, _y) in positions.items():
406
+ """
407
+ 依目前的鬼與獵物組出每個人的 (角色, 目標 x, 目標 y)。逃跑方向取兩點連線的反向,
408
+ 讓地板模式(只用 x)與飄移/追游標模式(用整個點)都有合理目標。
409
+ Build each participant's (role, target_x, target_y). Runners head along
410
+ the line away from "it", which suits floor pets (x only) and wander or
411
+ chase pets (the whole point) alike.
412
+ """
413
+ it_x, it_y = positions[self.it]
414
+ prey_position = positions[prey] if prey is not None else (it_x, it_y)
415
+ roles = {self.it: (TAG_CHASE, float(prey_position[0]), float(prey_position[1]))}
416
+ for participant, (x, y) in positions.items():
410
417
  if participant == self.it:
411
418
  continue
412
- away = self.flee_distance if x >= it_x else -self.flee_distance
413
- roles[participant] = (TAG_FLEE, x + away)
419
+ dx, dy = x - it_x, y - it_y
420
+ length = (dx * dx + dy * dy) ** 0.5
421
+ if length < 1e-9: # standing on each other: just pick a direction
422
+ dx, dy, length = 1.0, 0.0, 1.0
423
+ roles[participant] = (
424
+ TAG_FLEE,
425
+ x + self.flee_distance * dx / length,
426
+ y + self.flee_distance * dy / length,
427
+ )
414
428
  return roles
415
429
 
416
430
 
@@ -666,6 +680,10 @@ class PetMotion:
666
680
  self.ground_feet = 0.0
667
681
  # 追向某個 x(用於朝同伴走過去玩耍)/ Horizontal target to walk toward (play).
668
682
  self.follow_target_x: Optional[float] = None
683
+ # 引導目標 (x, y):三種行為模式都會朝它移動(鬼抓人用),優先於各自的預設動線。
684
+ # Guidance point (x, y) honoured by every behaviour (used by the tag
685
+ # game); it takes precedence over each mode's own default movement.
686
+ self.guidance: Optional[Tuple[float, float]] = None
669
687
 
670
688
  @property
671
689
  def facing_left(self) -> bool:
@@ -713,6 +731,43 @@ class PetMotion:
713
731
  def set_target(self, x: float, y: float) -> None:
714
732
  self.target = (float(x), float(y))
715
733
 
734
+ def set_guidance(self, x: float, y: Optional[float] = None) -> None:
735
+ """
736
+ 設定引導目標;地板模式走向該 x,飄移/追游標模式則朝該點移動。
737
+ Set the guidance point: floor pets walk toward its x, wander and chase
738
+ pets steer toward the point itself.
739
+ """
740
+ centre_y = self.y + self.height / 2.0
741
+ self.guidance = (float(x), centre_y if y is None else float(y))
742
+ if self.behaviour == BEHAVIOUR_FLOOR:
743
+ self.follow_target_x = self.guidance[0]
744
+
745
+ def clear_guidance(self) -> None:
746
+ """取消引導,回到該模式原本的動線 / Drop the guidance and resume normal movement."""
747
+ self.guidance = None
748
+ self.follow_target_x = None
749
+
750
+ STEER_BLEND = 0.4
751
+
752
+ def _steer_toward(self, point: Tuple[float, float]) -> None:
753
+ """把速度往目標點轉一些(保持速率),轉彎自然而不是瞬間轉向。"""
754
+ target_x, target_y = point
755
+ dx = target_x - (self.x + self.width / 2.0)
756
+ dy = target_y - (self.y + self.height / 2.0)
757
+ distance = (dx * dx + dy * dy) ** 0.5
758
+ if distance < 1e-9:
759
+ return
760
+ desired_x = self.speed * dx / distance
761
+ desired_y = self.speed * dy / distance
762
+ blended_x = self.vx + self.STEER_BLEND * (desired_x - self.vx)
763
+ blended_y = self.vy + self.STEER_BLEND * (desired_y - self.vy)
764
+ magnitude = (blended_x * blended_x + blended_y * blended_y) ** 0.5
765
+ if magnitude < 1e-9:
766
+ self.vx, self.vy = desired_x, desired_y
767
+ return
768
+ self.vx = self.speed * blended_x / magnitude
769
+ self.vy = self.speed * blended_y / magnitude
770
+
716
771
  def throw(self, vx: float, vy: float) -> None:
717
772
  """拖曳放開後以動量丟出(只在重力模式有意義)。"""
718
773
  self.vx = float(vx)
@@ -945,6 +1000,8 @@ class PetMotion:
945
1000
  def _step_wander(self, left, top, right, bottom) -> Tuple[int, int]:
946
1001
  if abs(self.vy) < 1e-9:
947
1002
  self.vy = float(self.speed)
1003
+ if self.guidance is not None:
1004
+ self._steer_toward(self.guidance)
948
1005
  self.x += self.vx
949
1006
  self.y += self.vy
950
1007
  if self.x <= left:
@@ -963,14 +1020,19 @@ class PetMotion:
963
1020
 
964
1021
  # --- chase cursor ---
965
1022
  def _step_chase(self, left, top, right, bottom) -> Tuple[int, int]:
966
- if self.target is None:
1023
+ # 被引導時(鬼抓人)以引導目標為準,而不是游標
1024
+ # While guided (tag game) the guidance point wins over the cursor.
1025
+ chase_target = self.guidance if self.guidance is not None else self.target
1026
+ if chase_target is None:
967
1027
  return int(self.x), int(self.y)
968
- target_x, target_y = self.target
1028
+ target_x, target_y = chase_target
969
1029
  dx = target_x - (self.x + self.width / 2)
970
1030
  dy = target_y - (self.y + self.height / 2)
971
1031
  distance = (dx * dx + dy * dy) ** 0.5
972
1032
  if distance <= self.CHASE_STOP_DISTANCE:
973
- self.asleep = True
1033
+ # 追到游標會睡著;玩鬼抓人時只是站在對方身上,不睡。
1034
+ # Catching the cursor means a nap; catching a playmate does not.
1035
+ self.asleep = self.guidance is None
974
1036
  return int(self.x), int(self.y)
975
1037
  self.asleep = False
976
1038
  step = float(self.speed) * self.CHASE_SPEED_FACTOR
@@ -1099,8 +1161,9 @@ class DesktopPetWidget(BaseWidget):
1099
1161
  PLAY_CHANCE = 0.25
1100
1162
 
1101
1163
  def set_pet_window_flag(self) -> None:
1102
- """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂。"""
1164
+ """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂,也不參與全域鎖定。"""
1103
1165
  apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
1166
+ self.overlay_lockable = False
1104
1167
 
1105
1168
  def center(self) -> Tuple[int, int]:
1106
1169
  return (self.x() + self.width() // 2, self.y() + self.height() // 2)
@@ -1111,17 +1174,17 @@ class DesktopPetWidget(BaseWidget):
1111
1174
 
1112
1175
  def apply_tag_role(self, role) -> None:
1113
1176
  """
1114
- 套用鬼抓人角色 (角色, 目標 x);None 代表沒在玩,恢復平常的同伴互動。
1177
+ 套用鬼抓人角色 (角色, 目標 x, 目標 y);None 代表沒在玩,恢復平常的同伴互動。
1115
1178
  Apply a tag role; None means the game is off and normal peer play resumes.
1116
1179
  """
1117
1180
  if role is None:
1118
1181
  if self._tag_role is not None:
1119
1182
  self._tag_role = None
1120
- self.motion.clear_follow()
1183
+ self.motion.clear_guidance()
1121
1184
  return
1122
- self._tag_role, target_x = role
1185
+ self._tag_role, target_x, target_y = role
1123
1186
  self.motion.wake()
1124
- self.motion.set_follow(target_x)
1187
+ self.motion.set_guidance(target_x, target_y)
1125
1188
 
1126
1189
  def on_tagged(self) -> None:
1127
1190
  """換自己當鬼時喊一句 / Shout when the tag lands on this pet."""
@@ -41,6 +41,11 @@ class TextWidget(BaseWidget):
41
41
  self._marquee_timer = QTimer(self)
42
42
  self._marquee_timer.timeout.connect(self._tick_marquee)
43
43
 
44
+ # 動態文字來源(時鐘/倒數/系統資訊…)/ Dynamic text source
45
+ self.text_source = None
46
+ self._source_timer = QTimer(self)
47
+ self._source_timer.timeout.connect(self.refresh_text)
48
+
44
49
  def set_font_variable(self, font_size: int = 100) -> None:
45
50
  front_engine_logger.info(f"[TextWidget] set_font_variable | font_size={font_size}")
46
51
  self.font_size = font_size
@@ -82,6 +87,35 @@ class TextWidget(BaseWidget):
82
87
  else:
83
88
  self._marquee_timer.stop()
84
89
 
90
+ def set_text_source(self, source) -> None:
91
+ """
92
+ 接上動態文字來源(TextSource);靜態來源只取一次文字,不啟動計時器。
93
+ Attach a dynamic text source; a static one is read once, with no timer.
94
+ """
95
+ front_engine_logger.info(f"[TextWidget] set_text_source | kind={getattr(source, 'kind', None)}")
96
+ self._source_timer.stop()
97
+ self.text_source = source
98
+ if source is None:
99
+ return
100
+ source.start()
101
+ self.refresh_text()
102
+ interval = getattr(source, "refresh_interval_ms", 0)
103
+ if interval > 0:
104
+ self._source_timer.start(int(interval))
105
+
106
+ def refresh_text(self) -> None:
107
+ """向來源取一次最新文字並重繪 / Pull the latest text and repaint."""
108
+ if self.text_source is None:
109
+ return
110
+ try:
111
+ text = self.text_source.text()
112
+ except Exception as error: # pragma: no cover - defensive around providers
113
+ front_engine_logger.warning(f"[TextWidget] text source failed: {error!r}")
114
+ return
115
+ if text != self.text:
116
+ self.text = text
117
+ self.update()
118
+
85
119
  def _tick_marquee(self) -> None:
86
120
  text_width = QFontMetrics(self.draw_font).horizontalAdvance(self.text)
87
121
  self.marquee_offset -= self.marquee_speed
@@ -102,6 +136,12 @@ class TextWidget(BaseWidget):
102
136
  self.text,
103
137
  )
104
138
 
139
+ def closeEvent(self, event) -> None:
140
+ for timer in (self._marquee_timer, self._source_timer):
141
+ if timer.isActive():
142
+ timer.stop()
143
+ super().closeEvent(event)
144
+
105
145
  def draw_content(self, painter: QPainter) -> None:
106
146
  painter.setFont(self.draw_font)
107
147
  if self.outline:
@@ -0,0 +1,68 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from PySide6.QtCore import Qt
5
+ from PySide6.QtGui import QIcon
6
+ from PySide6.QtWidgets import QWidget
7
+
8
+ _OVERLAY_BASE_FLAGS = (
9
+ Qt.WindowType.WindowTransparentForInput
10
+ | Qt.WindowType.FramelessWindowHint
11
+ | Qt.WindowType.Tool
12
+ )
13
+
14
+
15
+ def apply_overlay_window_flags(
16
+ widget: QWidget,
17
+ show_on_bottom: bool = False,
18
+ allow_input: bool = False,
19
+ ) -> None:
20
+ """
21
+ Apply the shared "overlay" window flag combination to a widget.
22
+
23
+ allow_input=True omits WindowTransparentForInput so the widget can still
24
+ receive mouse/keyboard (used by the web overlay when the user opts in).
25
+ """
26
+ flags = Qt.WindowType.FramelessWindowHint | Qt.WindowType.Tool
27
+ if not allow_input:
28
+ flags |= Qt.WindowType.WindowTransparentForInput
29
+ if show_on_bottom:
30
+ flags |= Qt.WindowType.WindowStaysOnBottomHint
31
+ else:
32
+ flags |= Qt.WindowType.WindowStaysOnTopHint
33
+ widget.setWindowFlags(flags)
34
+ # 記住這次的選擇,之後鎖定/解鎖時才能保持同樣的堆疊層級
35
+ # Remember the choice so lock/unlock keeps the same stacking level.
36
+ widget.overlay_show_on_bottom = bool(show_on_bottom)
37
+ widget.overlay_locked = not bool(allow_input)
38
+
39
+
40
+ def set_overlay_locked(widget: QWidget, locked: bool) -> bool:
41
+ """
42
+ 鎖定覆蓋層=點擊穿透(滑鼠事件直接落到底下的程式);解鎖=可以接收滑鼠、
43
+ 用拖曳調整位置。回傳是否真的切換(不可鎖定的覆蓋層如寵物會回傳 False)。
44
+
45
+ Locking an overlay makes it click-through so clicks reach whatever is
46
+ behind it; unlocking lets it take the mouse so it can be dragged into
47
+ place. Returns False for overlays that opt out of locking (e.g. the pet).
48
+ """
49
+ if not getattr(widget, "overlay_lockable", True):
50
+ return False
51
+ was_visible = widget.isVisible()
52
+ apply_overlay_window_flags(
53
+ widget,
54
+ show_on_bottom=getattr(widget, "overlay_show_on_bottom", False),
55
+ allow_input=not locked,
56
+ )
57
+ # 變更 window flags 會讓視窗被隱藏,原本看得見的要重新顯示
58
+ # Changing window flags hides the window, so show it again if it was visible.
59
+ if was_visible:
60
+ widget.show()
61
+ return True
62
+
63
+
64
+ def load_overlay_icon(widget: QWidget) -> None:
65
+ """Set the shared frontengine.ico as the widget's window icon if present."""
66
+ icon_path = Path(os.getcwd()) / "frontengine.ico"
67
+ if icon_path.exists() and icon_path.is_file():
68
+ widget.setWindowIcon(QIcon(str(icon_path)))
@@ -30,6 +30,8 @@ _ACTION_LABELS: Dict[str, Tuple[str, str]] = {
30
30
  "mute_all": ("control_center_mute_all", "Mute all"),
31
31
  "opacity_up": ("hotkey_opacity_up", "Opacity up"),
32
32
  "opacity_down": ("hotkey_opacity_down", "Opacity down"),
33
+ "dashboard_next": ("web_dashboard_next", "Next page"),
34
+ "toggle_lock": ("hotkey_toggle_lock", "Lock / unlock overlays"),
33
35
  }
34
36
 
35
37
 
@@ -321,6 +321,10 @@ class FrontEngineMainUI(QMainWindow):
321
321
  self.control_center_ui.step_opacity_all(0.1)
322
322
  elif action == "opacity_down":
323
323
  self.control_center_ui.step_opacity_all(-0.1)
324
+ elif action == "dashboard_next":
325
+ self.web_setting_ui.show_next_dashboard_page()
326
+ elif action == "toggle_lock":
327
+ self.control_center_ui.toggle_lock_all()
324
328
 
325
329
  def _on_fullscreen_changed(self, fullscreen_active: bool) -> None:
326
330
  """
@@ -1,6 +1,7 @@
1
1
  from PySide6.QtCore import QTimer, Qt
2
2
  from PySide6.QtWidgets import QGridLayout, QWidget, QPushButton, QTextEdit, QScrollArea
3
3
 
4
+ from frontengine.show.window_helpers import set_overlay_locked
4
5
  from frontengine.ui.color.global_color import error_color, output_color
5
6
  from frontengine.ui.page.gif.gif_setting_ui import GIFSettingUI
6
7
  from frontengine.ui.page.image.image_setting_ui import ImageSettingUI
@@ -10,6 +11,7 @@ from frontengine.ui.page.sound_player.sound_player_setting_ui import SoundPlayer
10
11
  from frontengine.ui.page.text.text_setting_ui import TextSettingUI
11
12
  from frontengine.ui.page.video.video_setting_ui import VideoSettingUI
12
13
  from frontengine.ui.page.web.web_setting_ui import WEBSettingUI
14
+ from frontengine.user_setting.user_setting_file import clear_overlay_geometry
13
15
  from frontengine.utils.logging.loggin_instance import front_engine_logger
14
16
  from frontengine.utils.multi_language.language_wrapper import language_wrapper
15
17
  from frontengine.utils.redirect_manager.redirect_manager_class import redirect_manager_instance
@@ -74,6 +76,13 @@ class ControlCenterUI(QWidget):
74
76
  # 全域不透明度(None 表示尚未使用過)/ Global opacity level (None until first used)
75
77
  self._global_opacity = None
76
78
  self.mute_all_button = self._create_button("control_center_mute_all", self.toggle_mute_all)
79
+ # 覆蓋層鎖定(點擊穿透)與綠幕背景 / Overlay lock (click-through) and chroma key
80
+ self._overlays_locked = True
81
+ self._chroma_key = False
82
+ self.lock_all_button = self._create_button("control_center_unlock_all", self.toggle_lock_all)
83
+ self.chroma_key_button = self._create_button("control_center_chroma_key_on", self.toggle_chroma_key)
84
+ self.reset_positions_button = self._create_button(
85
+ "control_center_reset_positions", self.reset_overlay_positions)
77
86
  self.clear_all_button = self._create_button("control_center_close_all", self.clear_all)
78
87
 
79
88
  # Log panel
@@ -96,8 +105,11 @@ class ControlCenterUI(QWidget):
96
105
  self.grid_layout.addWidget(self.hide_all_button, 7, 0)
97
106
  self.grid_layout.addWidget(self.show_all_button, 8, 0)
98
107
  self.grid_layout.addWidget(self.mute_all_button, 9, 0)
99
- self.grid_layout.addWidget(self.clear_all_button, 10, 0)
100
- self.grid_layout.addWidget(self.log_panel_scroll_area, 0, 1, 11, 1) # 明確指定 rowSpan=11, colSpan=1
108
+ self.grid_layout.addWidget(self.lock_all_button, 10, 0)
109
+ self.grid_layout.addWidget(self.chroma_key_button, 11, 0)
110
+ self.grid_layout.addWidget(self.reset_positions_button, 12, 0)
111
+ self.grid_layout.addWidget(self.clear_all_button, 13, 0)
112
+ self.grid_layout.addWidget(self.log_panel_scroll_area, 0, 1, 14, 1) # 明確指定 rowSpan, colSpan=1
101
113
  self.setLayout(self.grid_layout)
102
114
 
103
115
  # Redirect
@@ -225,6 +237,59 @@ class ControlCenterUI(QWidget):
225
237
  """切換全域靜音狀態 / Toggle the global mute state."""
226
238
  self.set_mute_all(not getattr(self, "_muted", False))
227
239
 
240
+ # --- lock / chroma key / positions -----------------------------------
241
+ CHROMA_KEY_COLOR = "#00ff00"
242
+
243
+ def set_lock_all(self, locked: bool) -> None:
244
+ """
245
+ 鎖定=所有覆蓋層點擊穿透;解鎖=可以用滑鼠把覆蓋層拖到想要的位置,
246
+ 放開時位置會被記住。寵物不受影響(它本來就要接收滑鼠)。
247
+ Locked overlays are click-through; unlocked ones can be dragged into
248
+ place and remember where they were dropped. The pet is left alone as it
249
+ needs the mouse either way.
250
+ """
251
+ front_engine_logger.info(f"ControlCenterUI set_lock_all | locked={locked}")
252
+ self._overlays_locked = bool(locked)
253
+ self._for_each_overlay(lambda widget: set_overlay_locked(widget, self._overlays_locked))
254
+ self.lock_all_button.setText(
255
+ language_wrapper.language_word_dict.get(
256
+ "control_center_unlock_all" if self._overlays_locked else "control_center_lock_all",
257
+ "Unlock overlays" if self._overlays_locked else "Lock overlays",
258
+ )
259
+ )
260
+
261
+ def toggle_lock_all(self) -> None:
262
+ """切換覆蓋層鎖定狀態 / Toggle the overlay lock."""
263
+ self.set_lock_all(not getattr(self, "_overlays_locked", True))
264
+
265
+ def set_chroma_key(self, enabled: bool) -> None:
266
+ """開關綠幕背景(給 OBS 之類的軟體去背用)。"""
267
+ front_engine_logger.info(f"ControlCenterUI set_chroma_key | enabled={enabled}")
268
+ self._chroma_key = bool(enabled)
269
+ color = self.CHROMA_KEY_COLOR if self._chroma_key else None
270
+
271
+ def apply(widget) -> None:
272
+ setter = getattr(widget, "set_background_color", None)
273
+ if setter is not None:
274
+ setter(color)
275
+
276
+ self._for_each_overlay(apply)
277
+ self.chroma_key_button.setText(
278
+ language_wrapper.language_word_dict.get(
279
+ "control_center_chroma_key_off" if self._chroma_key else "control_center_chroma_key_on",
280
+ "Chroma key off" if self._chroma_key else "Chroma key on",
281
+ )
282
+ )
283
+
284
+ def toggle_chroma_key(self) -> None:
285
+ """切換綠幕背景 / Toggle the chroma key background."""
286
+ self.set_chroma_key(not getattr(self, "_chroma_key", False))
287
+
288
+ def reset_overlay_positions(self) -> None:
289
+ """忘掉記住的覆蓋層位置,之後開的覆蓋層回到各自的預設顯示方式。"""
290
+ front_engine_logger.info("ControlCenterUI reset_overlay_positions")
291
+ clear_overlay_geometry()
292
+
228
293
  def _sample_overlay_opacity(self) -> float:
229
294
  """取一個現有覆蓋層的不透明度作為起始值 / Seed from an existing overlay's opacity."""
230
295
  for widget_list in self._all_overlay_widget_lists():