frontengine 1.0.29__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.29 → frontengine-1.0.30}/PKG-INFO +1 -1
  2. frontengine-1.0.30/frontengine/show/base_widget.py +125 -0
  3. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/pet/desktop_pet.py +2 -1
  4. {frontengine-1.0.29 → 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.29 → frontengine-1.0.30}/frontengine/ui/dialog/hotkey_settings_dialog.py +2 -0
  7. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/main_ui.py +4 -0
  8. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/control_center/control_center_ui.py +67 -2
  9. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/text/text_setting_ui.py +81 -1
  10. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/web/web_setting_ui.py +114 -2
  11. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/user_setting/user_setting_file.py +34 -1
  12. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/english.py +29 -0
  13. {frontengine-1.0.29 → 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.29 → 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.29 → frontengine-1.0.30}/frontengine.egg-info/PKG-INFO +1 -1
  22. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine.egg-info/SOURCES.txt +6 -0
  23. {frontengine-1.0.29 → frontengine-1.0.30}/pyproject.toml +1 -1
  24. frontengine-1.0.29/frontengine/show/base_widget.py +0 -47
  25. frontengine-1.0.29/frontengine/show/window_helpers.py +0 -40
  26. {frontengine-1.0.29 → frontengine-1.0.30}/LICENSE +0 -0
  27. {frontengine-1.0.29 → frontengine-1.0.30}/README.md +0 -0
  28. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/__init__.py +0 -0
  29. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/__main__.py +0 -0
  30. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/__init__.py +0 -0
  31. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/gif/__init__.py +0 -0
  32. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/gif/paint_gif.py +0 -0
  33. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/image/__init__.py +0 -0
  34. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/image/paint_image.py +0 -0
  35. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/load/__init__.py +0 -0
  36. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/load/load_someone_make_ui.py +0 -0
  37. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/overlay_factory.py +0 -0
  38. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/particle/__init__.py +0 -0
  39. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/particle/particle_ui.py +0 -0
  40. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/pet/__init__.py +0 -0
  41. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/scene/__init__.py +0 -0
  42. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  43. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/scene/extend_graphic_view.py +0 -0
  44. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/scene/scene.py +0 -0
  45. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/sound_player/__init__.py +0 -0
  46. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/sound_player/sound_effect.py +0 -0
  47. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/sound_player/sound_player.py +0 -0
  48. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/text/__init__.py +0 -0
  49. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/video/__init__.py +0 -0
  50. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/video/video_player.py +0 -0
  51. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/web/__init__.py +0 -0
  52. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/show/web/webview.py +0 -0
  53. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/system_tray/__init__.py +0 -0
  54. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/system_tray/extend_system_tray.py +0 -0
  55. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/__init__.py +0 -0
  56. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/color/__init__.py +0 -0
  57. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/color/global_color.py +0 -0
  58. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/dialog/__init__.py +0 -0
  59. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  60. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  61. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/menu/__init__.py +0 -0
  62. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/menu/help_menu.py +0 -0
  63. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/menu/how_to_menu.py +0 -0
  64. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/menu/language_menu.py +0 -0
  65. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/menu/preset_menu.py +0 -0
  66. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/menu/settings_menu.py +0 -0
  67. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/__init__.py +0 -0
  68. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/control_center/__init__.py +0 -0
  69. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/gif/__init__.py +0 -0
  70. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  71. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/image/__init__.py +0 -0
  72. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  73. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/particle/__init__.py +0 -0
  74. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  75. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/pet/__init__.py +0 -0
  76. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
  77. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  78. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  79. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  80. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  81. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  82. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  83. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  84. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  85. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  86. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  87. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  88. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  89. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/sound_player/__init__.py +0 -0
  90. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  91. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/text/__init__.py +0 -0
  92. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/utils.py +0 -0
  93. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/video/__init__.py +0 -0
  94. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  95. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/ui/page/web/__init__.py +0 -0
  96. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/user_setting/__init__.py +0 -0
  97. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/user_setting/preset_repository.py +0 -0
  98. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/user_setting/scene_setting.py +0 -0
  99. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/__init__.py +0 -0
  100. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/audio_meter/__init__.py +0 -0
  101. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  102. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  103. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  104. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/browser/__init__.py +0 -0
  105. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/browser/browser.py +0 -0
  106. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/critical_exit/__init__.py +0 -0
  107. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  108. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  109. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  110. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/exception/__init__.py +0 -0
  111. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/exception/exception_tags.py +0 -0
  112. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/exception/exceptions.py +0 -0
  113. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/file/__init__.py +0 -0
  114. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/file/open/__init__.py +0 -0
  115. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/file/open/open_file.py +0 -0
  116. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/file/save/__init__.py +0 -0
  117. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/file/save/save_file.py +0 -0
  118. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/hotkey/__init__.py +0 -0
  119. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  120. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/json/__init__.py +0 -0
  121. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/json/json_file.py +0 -0
  122. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/json/json_repository.py +0 -0
  123. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/json_format/__init__.py +0 -0
  124. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/json_format/json_process.py +0 -0
  125. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/logging/__init__.py +0 -0
  126. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/logging/loggin_instance.py +0 -0
  127. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/__init__.py +0 -0
  128. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/france.py +0 -0
  129. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/germany.py +0 -0
  130. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/italy.py +0 -0
  131. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  132. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/russian.py +0 -0
  133. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  134. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/preset_schedule/__init__.py +0 -0
  135. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  136. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/redirect_manager/__init__.py +0 -0
  137. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  138. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/smart_pause/__init__.py +0 -0
  139. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  140. {frontengine-1.0.29/frontengine/utils/theme_schedule → frontengine-1.0.30/frontengine/utils/system_stats}/__init__.py +0 -0
  141. {frontengine-1.0.29/frontengine/worker → frontengine-1.0.30/frontengine/utils/text_source}/__init__.py +0 -0
  142. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  143. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine/worker/qthread_worker.py +0 -0
  144. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine.egg-info/dependency_links.txt +0 -0
  145. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine.egg-info/entry_points.txt +0 -0
  146. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine.egg-info/requires.txt +0 -0
  147. {frontengine-1.0.29 → frontengine-1.0.30}/frontengine.egg-info/top_level.txt +0 -0
  148. {frontengine-1.0.29 → 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.29
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."""
@@ -1161,8 +1161,9 @@ class DesktopPetWidget(BaseWidget):
1161
1161
  PLAY_CHANCE = 0.25
1162
1162
 
1163
1163
  def set_pet_window_flag(self) -> None:
1164
- """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂。"""
1164
+ """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂,也不參與全域鎖定。"""
1165
1165
  apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
1166
+ self.overlay_lockable = False
1166
1167
 
1167
1168
  def center(self) -> Tuple[int, int]:
1168
1169
  return (self.x() + self.width() // 2, self.y() + self.height() // 2)
@@ -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():
@@ -12,6 +12,12 @@ from frontengine.ui.page.utils import (
12
12
  )
13
13
  from frontengine.utils.logging.loggin_instance import front_engine_logger
14
14
  from frontengine.utils.multi_language.language_wrapper import language_wrapper
15
+ from frontengine.utils.system_stats.system_stats import system_stats
16
+ from frontengine.utils.text_source.text_source import (
17
+ DEFAULT_TEMPLATES, SOURCE_CLOCK, SOURCE_COUNTDOWN, SOURCE_DATE, SOURCE_STATIC,
18
+ SOURCE_STOPWATCH, SOURCE_SYSTEM, SOURCE_WEATHER, TextSource,
19
+ )
20
+ from frontengine.utils.weather.weather_service import current_weather, lookup_city, weather_service
15
21
 
16
22
  # 名稱 -> HEX 顏色 / Named text colors -> hex
17
23
  _TEXT_COLORS = [
@@ -97,6 +103,29 @@ class TextSettingUI(QWidget):
97
103
  )
98
104
  self.target_monitor_combobox = build_target_monitor_combobox()
99
105
 
106
+ # 文字來源:靜態字串或時鐘/日期/倒數/碼表/系統資訊/天氣
107
+ # Text source: a fixed string, or a clock/date/countdown/stopwatch/stats/weather feed.
108
+ self.text_source_label = QLabel(
109
+ language_wrapper.language_word_dict.get("text_source_label", "Text source"))
110
+ self.text_source_combobox = QComboBox()
111
+ for kind, fallback in (
112
+ (SOURCE_STATIC, "Static text"), (SOURCE_CLOCK, "Clock"), (SOURCE_DATE, "Date"),
113
+ (SOURCE_COUNTDOWN, "Countdown"), (SOURCE_STOPWATCH, "Stopwatch"),
114
+ (SOURCE_SYSTEM, "System stats"), (SOURCE_WEATHER, "Weather"),
115
+ ):
116
+ self.text_source_combobox.addItem(
117
+ language_wrapper.language_word_dict.get(f"text_source_{kind}", fallback), kind)
118
+ self.text_source_combobox.currentIndexChanged.connect(self._update_source_hint)
119
+ self.text_source_hint_label = QLabel("")
120
+ self.text_source_hint_label.setWordWrap(True)
121
+
122
+ # 天氣地點(只有天氣來源會用到)/ Weather location (only used by the weather source)
123
+ self.weather_location_label = QLabel(
124
+ language_wrapper.language_word_dict.get("weather_location_label", "Weather city"))
125
+ self.weather_location_edit = QLineEdit()
126
+ self.weather_location_edit.setPlaceholderText(
127
+ language_wrapper.language_word_dict.get("weather_location_hint", "e.g. Taipei"))
128
+
100
129
  # Layout
101
130
  self.grid_layout.addWidget(self.opacity_label, 0, 0)
102
131
  self.grid_layout.addWidget(self.opacity_slider_value_label, 0, 1)
@@ -121,11 +150,49 @@ class TextSettingUI(QWidget):
121
150
  self.grid_layout.addWidget(self.marquee_speed_combobox, 8, 2)
122
151
  self.grid_layout.addWidget(self.outline_checkbox, 9, 0)
123
152
  self.grid_layout.addWidget(self.outline_color_combobox, 9, 1)
153
+ self.grid_layout.addWidget(self.text_source_label, 10, 0)
154
+ self.grid_layout.addWidget(self.text_source_combobox, 10, 1)
155
+ self.grid_layout.addWidget(self.weather_location_label, 11, 0)
156
+ self.grid_layout.addWidget(self.weather_location_edit, 11, 1)
157
+ self.grid_layout.addWidget(self.text_source_hint_label, 12, 0, 1, 3)
158
+ self._update_source_hint()
124
159
 
125
160
  def set_show_all_screen(self) -> None:
126
161
  front_engine_logger.info("[TextSettingUI] set_show_all_screen")
127
162
  self.show_all_screen = self.show_on_all_screen_checkbox.isChecked()
128
163
 
164
+ def _update_source_hint(self) -> None:
165
+ """依所選來源說明文字欄位的意義(樣板、倒數分鐘數…)。"""
166
+ kind = self.text_source_combobox.currentData() or SOURCE_STATIC
167
+ default = DEFAULT_TEMPLATES.get(kind, "")
168
+ hint = language_wrapper.language_word_dict.get(f"text_source_hint_{kind}", "")
169
+ self.text_source_hint_label.setText(f"{hint} ({default})" if default else hint)
170
+ self.weather_location_label.setVisible(kind == SOURCE_WEATHER)
171
+ self.weather_location_edit.setVisible(kind == SOURCE_WEATHER)
172
+
173
+ def _build_text_source(self) -> TextSource:
174
+ """依目前設定組出文字來源;天氣來源會先把地名解析成座標。"""
175
+ kind = self.text_source_combobox.currentData() or SOURCE_STATIC
176
+ if kind == SOURCE_WEATHER:
177
+ self._apply_weather_location()
178
+ return TextSource(
179
+ kind=kind,
180
+ template=self.line_edit.text(),
181
+ stats_provider=system_stats,
182
+ weather_provider=current_weather,
183
+ )
184
+
185
+ def _apply_weather_location(self) -> None:
186
+ """把使用者填的地名換成座標;查不到就沿用預設地點。"""
187
+ city = self.weather_location_edit.text().strip()
188
+ if not city:
189
+ return
190
+ found = lookup_city(city)
191
+ if found is None:
192
+ front_engine_logger.warning(f"[TextSettingUI] weather city not found: {city}")
193
+ return
194
+ weather_service().set_location(found[0], found[1])
195
+
129
196
  def _create_text_widget(self) -> TextWidget:
130
197
  front_engine_logger.info("[TextSettingUI] _create_text_widget")
131
198
  text_widget = TextWidget(
@@ -144,6 +211,7 @@ class TextSettingUI(QWidget):
144
211
  self.marquee_checkbox.isChecked(),
145
212
  int(self.marquee_speed_combobox.currentText()),
146
213
  )
214
+ text_widget.set_text_source(self._build_text_source())
147
215
  self.text_widget_list.append(text_widget)
148
216
  return text_widget
149
217
 
@@ -170,6 +238,8 @@ class TextSettingUI(QWidget):
170
238
  "marquee_speed": self.marquee_speed_combobox.currentText(),
171
239
  "outline": self.outline_checkbox.isChecked(),
172
240
  "outline_color": self.outline_color_combobox.currentText(),
241
+ "text_source": self.text_source_combobox.currentData(),
242
+ "weather_location": self.weather_location_edit.text(),
173
243
  }
174
244
 
175
245
  def set_state(self, state: dict) -> None:
@@ -212,11 +282,21 @@ class TextSettingUI(QWidget):
212
282
  index = self.outline_color_combobox.findText(str(state["outline_color"]))
213
283
  if index >= 0:
214
284
  self.outline_color_combobox.setCurrentIndex(index)
285
+ if state.get("text_source") is not None:
286
+ index = self.text_source_combobox.findData(str(state["text_source"]))
287
+ if index >= 0:
288
+ self.text_source_combobox.setCurrentIndex(index)
289
+ if state.get("weather_location") is not None:
290
+ self.weather_location_edit.setText(str(state["weather_location"]))
215
291
 
216
292
  def start_draw_text_on_screen(self) -> None:
217
293
  front_engine_logger.info("[TextSettingUI] start_draw_text_on_screen")
218
294
 
219
- if not self.line_edit.text().strip():
295
+ # 動態來源不需要固定文字(樣板留白就用預設);只有靜態文字必須填。
296
+ # Dynamic sources fall back to their default template, so only static
297
+ # text has to be filled in.
298
+ is_static = (self.text_source_combobox.currentData() or SOURCE_STATIC) == SOURCE_STATIC
299
+ if is_static and not self.line_edit.text().strip():
220
300
  QMessageBox.warning(self, "Warning", language_wrapper.language_word_dict.get("not_prepare"))
221
301
  return
222
302