frontengine 1.0.29__tar.gz → 1.0.31__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of frontengine might be problematic. Click here for more details.
- {frontengine-1.0.29 → frontengine-1.0.31}/PKG-INFO +1 -1
- frontengine-1.0.31/frontengine/show/base_widget.py +125 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/pet/desktop_pet.py +231 -7
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/text/draw_text.py +40 -0
- frontengine-1.0.31/frontengine/show/window_helpers.py +68 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/dialog/hotkey_settings_dialog.py +2 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/main_ui.py +4 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/control_center/control_center_ui.py +67 -2
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/pet/pet_setting_ui.py +115 -1
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/text/text_setting_ui.py +81 -1
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/web/web_setting_ui.py +114 -2
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/user_setting/user_setting_file.py +34 -1
- frontengine-1.0.31/frontengine/utils/focus_timer/focus_timer.py +112 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/english.py +44 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/traditional_chinese.py +44 -0
- frontengine-1.0.31/frontengine/utils/pet_chat/pet_chat_service.py +162 -0
- frontengine-1.0.31/frontengine/utils/system_stats/__init__.py +0 -0
- frontengine-1.0.31/frontengine/utils/system_stats/system_stats.py +309 -0
- frontengine-1.0.31/frontengine/utils/text_source/__init__.py +0 -0
- frontengine-1.0.31/frontengine/utils/text_source/text_source.py +186 -0
- frontengine-1.0.31/frontengine/utils/theme_schedule/__init__.py +0 -0
- frontengine-1.0.31/frontengine/utils/weather/__init__.py +0 -0
- frontengine-1.0.31/frontengine/utils/weather/weather_service.py +184 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/web_url.py +26 -0
- frontengine-1.0.31/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine.egg-info/PKG-INFO +1 -1
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine.egg-info/SOURCES.txt +10 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/pyproject.toml +1 -1
- frontengine-1.0.29/frontengine/show/base_widget.py +0 -47
- frontengine-1.0.29/frontengine/show/window_helpers.py +0 -40
- {frontengine-1.0.29 → frontengine-1.0.31}/LICENSE +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/README.md +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/__main__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/gif/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/image/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/load/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/particle/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/pet/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/scene/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/sound_player/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/text/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/video/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/web/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/system_tray/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/color/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/dialog/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/menu/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/menu/settings_menu.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/control_center/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/gif/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/image/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/particle/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/pet/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/sound_player/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/text/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/video/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/ui/page/web/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/user_setting/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/audio_meter/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/browser/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/critical_exit/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/file/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.29/frontengine/utils/hotkey → frontengine-1.0.31/frontengine/utils/focus_timer}/__init__.py +0 -0
- {frontengine-1.0.29/frontengine/utils/json → frontengine-1.0.31/frontengine/utils/hotkey}/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.29/frontengine/utils/logging → frontengine-1.0.31/frontengine/utils/json}/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.29/frontengine/utils/multi_language → frontengine-1.0.31/frontengine/utils/logging}/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.29/frontengine/utils/preset_schedule → frontengine-1.0.31/frontengine/utils/multi_language}/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.29/frontengine/utils/redirect_manager → frontengine-1.0.31/frontengine/utils/pet_chat}/__init__.py +0 -0
- {frontengine-1.0.29/frontengine/utils/smart_pause → frontengine-1.0.31/frontengine/utils/preset_schedule}/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.29/frontengine/utils/theme_schedule → frontengine-1.0.31/frontengine/utils/redirect_manager}/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.29/frontengine/worker → frontengine-1.0.31/frontengine/utils/smart_pause}/__init__.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.29 → frontengine-1.0.31}/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.31
|
|
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."""
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import random as _random_module
|
|
2
3
|
import sys
|
|
3
4
|
from datetime import datetime, timedelta
|
|
4
5
|
from pathlib import Path
|
|
6
|
+
from urllib.parse import urlparse
|
|
5
7
|
from typing import Optional, Tuple
|
|
6
8
|
|
|
7
9
|
from PySide6.QtCore import Qt, QRect, QTimer, QUrl, Signal
|
|
@@ -83,6 +85,10 @@ def scan_pet_pack(folder: str) -> dict:
|
|
|
83
85
|
return result
|
|
84
86
|
|
|
85
87
|
|
|
88
|
+
# 可以請寵物幫忙開啟的網址協定 / URL schemes the pet will open for you
|
|
89
|
+
_WEB_SCHEMES = frozenset({"http", "https"})
|
|
90
|
+
|
|
91
|
+
|
|
86
92
|
# 拖進寵物的檔案分類 / What a file dropped onto the pet means
|
|
87
93
|
DROP_SPRITE = "sprite" # 圖片或動作包 -> 換一套外觀
|
|
88
94
|
DROP_FOOD = "food" # 其他檔案 -> 當成食物餵給寵物
|
|
@@ -142,6 +148,55 @@ def classify_food(path) -> str:
|
|
|
142
148
|
return FOOD_SNACK
|
|
143
149
|
|
|
144
150
|
|
|
151
|
+
# 動作包設定檔:放在動作包資料夾裡,用來描述這隻寵物的個性與參數
|
|
152
|
+
# Pack manifest: sits inside a pet pack folder describing that pet's traits.
|
|
153
|
+
PACK_MANIFEST_NAME = "pet.json"
|
|
154
|
+
_MANIFEST_NUMBERS = ("size", "speed")
|
|
155
|
+
_MANIFEST_FLAGS = ("climb", "talk", "sit_on_windows")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_pet_manifest(folder) -> dict:
|
|
159
|
+
"""
|
|
160
|
+
讀取動作包裡的 pet.json(可選)。只取認得的欄位,型別不符就略過,
|
|
161
|
+
讀不到或格式錯誤一律回傳空 dict,讓動作包永遠不會弄壞寵物。
|
|
162
|
+
|
|
163
|
+
Read a pack's optional pet.json. Only known, well-typed fields are kept and
|
|
164
|
+
anything unreadable yields {}, so a pack can never break the pet.
|
|
165
|
+
"""
|
|
166
|
+
manifest: dict = {}
|
|
167
|
+
try:
|
|
168
|
+
path = Path(folder) / PACK_MANIFEST_NAME
|
|
169
|
+
if not path.is_file():
|
|
170
|
+
return manifest
|
|
171
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
172
|
+
raw = json.load(handle)
|
|
173
|
+
except (OSError, ValueError, TypeError):
|
|
174
|
+
return manifest
|
|
175
|
+
if not isinstance(raw, dict):
|
|
176
|
+
return manifest
|
|
177
|
+
for key in _MANIFEST_NUMBERS:
|
|
178
|
+
try:
|
|
179
|
+
if key in raw:
|
|
180
|
+
manifest[key] = max(1, int(raw[key]))
|
|
181
|
+
except (TypeError, ValueError):
|
|
182
|
+
continue
|
|
183
|
+
for key in _MANIFEST_FLAGS:
|
|
184
|
+
if isinstance(raw.get(key), bool):
|
|
185
|
+
manifest[key] = raw[key]
|
|
186
|
+
if isinstance(raw.get("name"), str):
|
|
187
|
+
manifest["name"] = raw["name"]
|
|
188
|
+
if isinstance(raw.get("sound"), str):
|
|
189
|
+
manifest["sound"] = raw["sound"]
|
|
190
|
+
lines = raw.get("lines")
|
|
191
|
+
if isinstance(lines, dict):
|
|
192
|
+
manifest["lines"] = {
|
|
193
|
+
str(pool): [str(line) for line in values if str(line).strip()]
|
|
194
|
+
for pool, values in lines.items()
|
|
195
|
+
if isinstance(values, list) and values
|
|
196
|
+
}
|
|
197
|
+
return manifest
|
|
198
|
+
|
|
199
|
+
|
|
145
200
|
def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_state: str) -> str:
|
|
146
201
|
"""依目前情形推導應顯示的視覺狀態 / Derive which sprite state to show."""
|
|
147
202
|
if dragging:
|
|
@@ -1101,9 +1156,21 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1101
1156
|
self._hunger = PetHunger(user_setting_dict.get("pet_hunger", 70))
|
|
1102
1157
|
self._hunger_warned = False
|
|
1103
1158
|
self._messages = self._load_messages()
|
|
1159
|
+
# 動作包自帶的設定(可含台詞、速度、體型、音效)
|
|
1160
|
+
# Traits shipped inside the pack (lines, speed, size, sound).
|
|
1161
|
+
self.manifest = read_pet_manifest(self.image_path) if self.image_path.is_dir() else {}
|
|
1162
|
+
self._apply_manifest()
|
|
1104
1163
|
self._volume = max(0.0, min(1.0, float(volume)))
|
|
1105
1164
|
self._sound: Optional[QSoundEffect] = None
|
|
1106
|
-
self._init_sound(sound_path)
|
|
1165
|
+
self._init_sound(self.manifest.get("sound_path", sound_path))
|
|
1166
|
+
# 語音朗讀(可選,缺少 QtTextToSpeech 時自動停用)
|
|
1167
|
+
# Optional spoken lines; disabled automatically without QtTextToSpeech.
|
|
1168
|
+
self._speech = None
|
|
1169
|
+
self._speak = False
|
|
1170
|
+
# AI 對話(可選,需環境變數金鑰;預設關閉)
|
|
1171
|
+
# Optional AI chat; needs an env-var key and is off by default.
|
|
1172
|
+
self._chat_service = None
|
|
1173
|
+
self._pending_chat_reply: Optional[str] = None
|
|
1107
1174
|
self._sit_on_windows = bool(sit_on_windows)
|
|
1108
1175
|
self._platform_tick = 0
|
|
1109
1176
|
# 音訊視覺化:依可注入的音量 provider 脈動 / Audio-reactive pulse
|
|
@@ -1152,6 +1219,11 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1152
1219
|
language_wrapper.language_word_dict.get("pet_set_reminder", "Set reminder..."), self)
|
|
1153
1220
|
self.reminder_action.triggered.connect(self._prompt_reminder)
|
|
1154
1221
|
self.menu.addAction(self.reminder_action)
|
|
1222
|
+
self.chat_action = QAction(
|
|
1223
|
+
language_wrapper.language_word_dict.get("pet_chat_menu", "Talk to me..."), self)
|
|
1224
|
+
self.chat_action.triggered.connect(self._prompt_chat)
|
|
1225
|
+
self.chat_action.setVisible(False) # shown only when a chat service is attached
|
|
1226
|
+
self.menu.addAction(self.chat_action)
|
|
1155
1227
|
self.close_action = QAction(language_wrapper.language_word_dict.get("control_center_close_all", "Close"), self)
|
|
1156
1228
|
self.close_action.triggered.connect(self.close)
|
|
1157
1229
|
self.menu.addAction(self.close_action)
|
|
@@ -1161,8 +1233,9 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1161
1233
|
PLAY_CHANCE = 0.25
|
|
1162
1234
|
|
|
1163
1235
|
def set_pet_window_flag(self) -> None:
|
|
1164
|
-
"""寵物需接收滑鼠以便拖曳,故 allow_input=True
|
|
1236
|
+
"""寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂,也不參與全域鎖定。"""
|
|
1165
1237
|
apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
|
|
1238
|
+
self.overlay_lockable = False
|
|
1166
1239
|
|
|
1167
1240
|
def center(self) -> Tuple[int, int]:
|
|
1168
1241
|
return (self.x() + self.width() // 2, self.y() + self.height() // 2)
|
|
@@ -1357,6 +1430,58 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1357
1430
|
if self._timeline_start is None:
|
|
1358
1431
|
self._timeline_start = self._now_provider()
|
|
1359
1432
|
|
|
1433
|
+
def _apply_manifest(self) -> None:
|
|
1434
|
+
"""套用動作包設定:台詞併入句庫,速度/體型/音效覆寫預設值。"""
|
|
1435
|
+
if not self.manifest:
|
|
1436
|
+
return
|
|
1437
|
+
front_engine_logger.info(f"[DesktopPetWidget] pack manifest | {self.manifest}")
|
|
1438
|
+
for pool, lines in (self.manifest.get("lines") or {}).items():
|
|
1439
|
+
self._messages[pool] = list(lines)
|
|
1440
|
+
if "speed" in self.manifest:
|
|
1441
|
+
self.motion.speed = max(1, int(self.manifest["speed"]))
|
|
1442
|
+
if "size" in self.manifest:
|
|
1443
|
+
self._base_size = max(16, int(self.manifest["size"]))
|
|
1444
|
+
self._apply_growth_size()
|
|
1445
|
+
if "climb" in self.manifest:
|
|
1446
|
+
self.motion.climb = bool(self.manifest["climb"])
|
|
1447
|
+
if "talk" in self.manifest:
|
|
1448
|
+
self._talk = bool(self.manifest["talk"])
|
|
1449
|
+
sound = self.manifest.get("sound")
|
|
1450
|
+
if sound:
|
|
1451
|
+
candidate = self.image_path / sound
|
|
1452
|
+
if candidate.is_file():
|
|
1453
|
+
self.manifest["sound_path"] = str(candidate)
|
|
1454
|
+
|
|
1455
|
+
def set_speech_enabled(self, enabled: bool) -> bool:
|
|
1456
|
+
"""
|
|
1457
|
+
開關語音朗讀;系統缺少 QtTextToSpeech 時回傳 False 並維持關閉。
|
|
1458
|
+
Toggle spoken lines, returning False when QtTextToSpeech is unavailable.
|
|
1459
|
+
"""
|
|
1460
|
+
if not enabled:
|
|
1461
|
+
self._speak = False
|
|
1462
|
+
return False
|
|
1463
|
+
if self._speech is None:
|
|
1464
|
+
try:
|
|
1465
|
+
from PySide6.QtTextToSpeech import QTextToSpeech
|
|
1466
|
+
|
|
1467
|
+
self._speech = QTextToSpeech(self)
|
|
1468
|
+
except Exception as error: # pragma: no cover - optional Qt module
|
|
1469
|
+
front_engine_logger.warning(f"[DesktopPetWidget] speech unavailable: {error!r}")
|
|
1470
|
+
self._speech = None
|
|
1471
|
+
self._speak = False
|
|
1472
|
+
return False
|
|
1473
|
+
self._speak = True
|
|
1474
|
+
return True
|
|
1475
|
+
|
|
1476
|
+
def _speak_text(self, text: str) -> None:
|
|
1477
|
+
"""把台詞念出來(未啟用或失敗時安靜略過)。"""
|
|
1478
|
+
if not self._speak or self._speech is None or not text:
|
|
1479
|
+
return
|
|
1480
|
+
try:
|
|
1481
|
+
self._speech.say(text)
|
|
1482
|
+
except Exception as error: # pragma: no cover - speech backend guard
|
|
1483
|
+
front_engine_logger.warning(f"[DesktopPetWidget] speech failed: {error!r}")
|
|
1484
|
+
|
|
1360
1485
|
def _load_messages(self) -> dict:
|
|
1361
1486
|
d = language_wrapper.language_word_dict
|
|
1362
1487
|
|
|
@@ -1381,6 +1506,11 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1381
1506
|
"away": lines("pet_chatter_away", "Still there?|I'll nap till you're back~|Zzz..."),
|
|
1382
1507
|
"levelup": lines("pet_chatter_levelup", "Level up!|I'm growing!|We're getting closer!"),
|
|
1383
1508
|
"costume": lines("pet_chatter_costume", "New look!|How do I look?|Nice fit!"),
|
|
1509
|
+
"fetch": lines("pet_chatter_fetch", "Fetch!|On it!|Opening that for you!"),
|
|
1510
|
+
"focus_start": lines("pet_chatter_focus_start", "Focus time!|Let's concentrate!|I'll wait here."),
|
|
1511
|
+
"focus_break": lines("pet_chatter_focus_break", "Break time!|Stretch a little!|Rest your eyes."),
|
|
1512
|
+
"focus_back": lines("pet_chatter_focus_back", "Back to it!|Round two!|Let's keep going."),
|
|
1513
|
+
"focus_done": lines("pet_chatter_focus_done", "Long break earned!|Great run!|Take a proper rest."),
|
|
1384
1514
|
"tag": lines("pet_chatter_tag", "You're it!|Caught you!|My turn to chase!"),
|
|
1385
1515
|
"any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
|
|
1386
1516
|
}
|
|
@@ -1488,6 +1618,53 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1488
1618
|
return
|
|
1489
1619
|
self.add_reminder(text.strip(), minutes)
|
|
1490
1620
|
|
|
1621
|
+
# --- optional AI chat --------------------------------------------------
|
|
1622
|
+
def set_chat_service(self, service) -> None:
|
|
1623
|
+
"""
|
|
1624
|
+
接上 AI 對話服務(None 表示關閉);服務不可用時右鍵選單不會出現該項目。
|
|
1625
|
+
Attach the AI chat service (None disables it); the menu entry only
|
|
1626
|
+
appears when the service is actually usable.
|
|
1627
|
+
"""
|
|
1628
|
+
self._chat_service = service
|
|
1629
|
+
usable = service is not None and bool(getattr(service, "available", lambda: False)())
|
|
1630
|
+
self.chat_action.setVisible(usable)
|
|
1631
|
+
|
|
1632
|
+
def _prompt_chat(self) -> None:
|
|
1633
|
+
"""問使用者要說什麼,然後在背景等回覆(不卡 UI)。"""
|
|
1634
|
+
from PySide6.QtWidgets import QInputDialog
|
|
1635
|
+
|
|
1636
|
+
if self._chat_service is None:
|
|
1637
|
+
return
|
|
1638
|
+
title = language_wrapper.language_word_dict.get("pet_chat_menu", "Talk to me...")
|
|
1639
|
+
text, ok = QInputDialog.getText(
|
|
1640
|
+
self, title, language_wrapper.language_word_dict.get("pet_chat_prompt", "Say something:"))
|
|
1641
|
+
if not ok or not text.strip():
|
|
1642
|
+
return
|
|
1643
|
+
self.ask_chat(text.strip())
|
|
1644
|
+
|
|
1645
|
+
def ask_chat(self, message: str) -> None:
|
|
1646
|
+
"""送出一句話並在收到回覆時說出來;失敗則說一句抱歉。"""
|
|
1647
|
+
if self._chat_service is None:
|
|
1648
|
+
return
|
|
1649
|
+
self.say(language_wrapper.language_word_dict.get("pet_chat_thinking", "Hmm, let me think..."))
|
|
1650
|
+
self._chat_service.ask_async(message, self.handle_chat_reply)
|
|
1651
|
+
|
|
1652
|
+
def handle_chat_reply(self, reply: Optional[str]) -> None:
|
|
1653
|
+
"""處理回覆(可能來自背景執行緒);空回覆時說一句抱歉。"""
|
|
1654
|
+
if reply:
|
|
1655
|
+
self._pending_chat_reply = reply
|
|
1656
|
+
else:
|
|
1657
|
+
self._pending_chat_reply = language_wrapper.language_word_dict.get(
|
|
1658
|
+
"pet_chat_failed", "I couldn't reach my thoughts just now.")
|
|
1659
|
+
# 回覆可能在背景執行緒抵達,透過計時器回到 UI 執行緒再顯示
|
|
1660
|
+
# The reply may arrive off-thread; hop back to the UI thread to show it.
|
|
1661
|
+
QTimer.singleShot(0, self._say_pending_chat_reply)
|
|
1662
|
+
|
|
1663
|
+
def _say_pending_chat_reply(self) -> None:
|
|
1664
|
+
if self._pending_chat_reply:
|
|
1665
|
+
self.say(self._pending_chat_reply)
|
|
1666
|
+
self._pending_chat_reply = None
|
|
1667
|
+
|
|
1491
1668
|
def _init_sound(self, sound_path: Optional[str]) -> None:
|
|
1492
1669
|
if not sound_path:
|
|
1493
1670
|
return
|
|
@@ -1611,6 +1788,7 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1611
1788
|
text = pick_message(datetime.now().hour, self._chatter_rng, self._messages, mood=mood)
|
|
1612
1789
|
if text:
|
|
1613
1790
|
self._bubble.show_message(text, self)
|
|
1791
|
+
self._speak_text(text)
|
|
1614
1792
|
|
|
1615
1793
|
def _on_tick(self) -> None:
|
|
1616
1794
|
if self._dragging:
|
|
@@ -1687,26 +1865,72 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1687
1865
|
return local_path
|
|
1688
1866
|
return None
|
|
1689
1867
|
|
|
1868
|
+
@staticmethod
|
|
1869
|
+
def dropped_link(mime_data) -> Optional[str]:
|
|
1870
|
+
"""
|
|
1871
|
+
從拖曳資料取出網址(拖連結到寵物身上請牠幫忙開啟);沒有則回傳 None。
|
|
1872
|
+
Pull an http(s) link out of a drop so the pet can open it for you.
|
|
1873
|
+
"""
|
|
1874
|
+
if mime_data is None:
|
|
1875
|
+
return None
|
|
1876
|
+
candidates = []
|
|
1877
|
+
if mime_data.hasUrls():
|
|
1878
|
+
candidates += [url.toString() for url in mime_data.urls() if not url.isLocalFile()]
|
|
1879
|
+
if mime_data.hasText():
|
|
1880
|
+
candidates.append(mime_data.text())
|
|
1881
|
+
for candidate in candidates:
|
|
1882
|
+
text = str(candidate or "").strip()
|
|
1883
|
+
# 用網址解析判斷通訊協定,比字串開頭比對穩,也不必在程式碼裡寫死協定前綴。
|
|
1884
|
+
# Parse the URL instead of prefix-matching: sturdier, and no scheme
|
|
1885
|
+
# literals baked into the source.
|
|
1886
|
+
try:
|
|
1887
|
+
scheme = urlparse(text).scheme.lower()
|
|
1888
|
+
except ValueError:
|
|
1889
|
+
continue
|
|
1890
|
+
if scheme in _WEB_SCHEMES:
|
|
1891
|
+
return text
|
|
1892
|
+
return None
|
|
1893
|
+
|
|
1894
|
+
def _accepts_drop(self, mime_data) -> bool:
|
|
1895
|
+
return self.dropped_path(mime_data) is not None or self.dropped_link(mime_data) is not None
|
|
1896
|
+
|
|
1690
1897
|
def dragEnterEvent(self, event) -> None:
|
|
1691
|
-
if self.
|
|
1898
|
+
if self._accepts_drop(event.mimeData()):
|
|
1692
1899
|
event.acceptProposedAction()
|
|
1693
1900
|
|
|
1694
1901
|
def dragMoveEvent(self, event) -> None:
|
|
1695
|
-
if self.
|
|
1902
|
+
if self._accepts_drop(event.mimeData()):
|
|
1696
1903
|
event.acceptProposedAction()
|
|
1697
1904
|
|
|
1698
1905
|
def dropEvent(self, event) -> None:
|
|
1699
1906
|
path = self.dropped_path(event.mimeData())
|
|
1700
|
-
if path is None
|
|
1907
|
+
link = self.dropped_link(event.mimeData()) if path is None else None
|
|
1908
|
+
if path is None and link is None:
|
|
1701
1909
|
return
|
|
1702
|
-
front_engine_logger.info(f"[DesktopPetWidget] dropEvent | path={path}")
|
|
1910
|
+
front_engine_logger.info(f"[DesktopPetWidget] dropEvent | path={path}, link={link}")
|
|
1703
1911
|
self.motion.wake()
|
|
1704
|
-
if
|
|
1912
|
+
if link is not None:
|
|
1913
|
+
self.fetch_link(link)
|
|
1914
|
+
elif classify_drop(path) == DROP_SPRITE:
|
|
1705
1915
|
self.change_sprite(path)
|
|
1706
1916
|
else:
|
|
1707
1917
|
self.feed(classify_food(path))
|
|
1708
1918
|
event.acceptProposedAction()
|
|
1709
1919
|
|
|
1920
|
+
def fetch_link(self, url: str) -> None:
|
|
1921
|
+
"""把拖進來的連結叼去瀏覽器開啟,並說一句 / Fetch a dropped link in the browser."""
|
|
1922
|
+
from frontengine.utils.browser.browser import open_browser
|
|
1923
|
+
|
|
1924
|
+
front_engine_logger.info(f"[DesktopPetWidget] fetch_link | url={url}")
|
|
1925
|
+
self._gain_affection(1)
|
|
1926
|
+
pool = self._messages.get("fetch") or []
|
|
1927
|
+
if pool:
|
|
1928
|
+
self.say(pool[self._chatter_rng.randrange(len(pool))])
|
|
1929
|
+
try:
|
|
1930
|
+
open_browser(url)
|
|
1931
|
+
except Exception as error: # pragma: no cover - browser boundary
|
|
1932
|
+
front_engine_logger.warning(f"[DesktopPetWidget] open_browser failed: {error!r}")
|
|
1933
|
+
|
|
1710
1934
|
def contextMenuEvent(self, event) -> None:
|
|
1711
1935
|
self.menu.popup(event.globalPos())
|
|
1712
1936
|
|
|
@@ -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
|
"""
|