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.
- {frontengine-1.0.28 → frontengine-1.0.30}/PKG-INFO +1 -1
- frontengine-1.0.30/frontengine/show/base_widget.py +125 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/pet/desktop_pet.py +77 -14
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/text/draw_text.py +40 -0
- frontengine-1.0.30/frontengine/show/window_helpers.py +68 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/hotkey_settings_dialog.py +2 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/main_ui.py +4 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/control_center/control_center_ui.py +67 -2
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/text/text_setting_ui.py +81 -1
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/web/web_setting_ui.py +114 -2
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/user_setting_file.py +34 -1
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/english.py +29 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/traditional_chinese.py +29 -0
- frontengine-1.0.30/frontengine/utils/system_stats/system_stats.py +309 -0
- frontengine-1.0.30/frontengine/utils/text_source/text_source.py +186 -0
- frontengine-1.0.30/frontengine/utils/theme_schedule/__init__.py +0 -0
- frontengine-1.0.30/frontengine/utils/weather/__init__.py +0 -0
- frontengine-1.0.30/frontengine/utils/weather/weather_service.py +184 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/web_url.py +26 -0
- frontengine-1.0.30/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/PKG-INFO +1 -1
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/SOURCES.txt +6 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/pyproject.toml +1 -1
- frontengine-1.0.28/frontengine/show/base_widget.py +0 -47
- frontengine-1.0.28/frontengine/show/window_helpers.py +0 -40
- {frontengine-1.0.28 → frontengine-1.0.30}/LICENSE +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/README.md +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/__main__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/gif/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/image/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/load/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/particle/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/pet/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/sound_player/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/text/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/video/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/web/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/system_tray/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/color/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/menu/settings_menu.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/control_center/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/gif/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/image/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/particle/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/pet/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/sound_player/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/text/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/video/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/web/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/browser/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/hotkey/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/logging/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/preset_schedule/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/redirect_manager/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/smart_pause/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.28/frontengine/utils/theme_schedule → frontengine-1.0.30/frontengine/utils/system_stats}/__init__.py +0 -0
- {frontengine-1.0.28/frontengine/worker → frontengine-1.0.30/frontengine/utils/text_source}/__init__.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.28 → frontengine-1.0.30}/frontengine.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
"""
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
"""
|
{frontengine-1.0.28 → frontengine-1.0.30}/frontengine/ui/page/control_center/control_center_ui.py
RENAMED
|
@@ -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.
|
|
100
|
-
self.grid_layout.addWidget(self.
|
|
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():
|