frontengine 1.0.31__tar.gz → 1.0.33__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.31 → frontengine-1.0.33}/PKG-INFO +1 -1
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/pet/desktop_pet.py +24 -93
- frontengine-1.0.33/frontengine/show/presentation/annotation_overlay.py +153 -0
- frontengine-1.0.33/frontengine/show/presentation/cursor_effects.py +166 -0
- frontengine-1.0.33/frontengine/show/presentation/keystroke_display.py +161 -0
- frontengine-1.0.33/frontengine/show/presentation/magnifier.py +127 -0
- frontengine-1.0.33/frontengine/show/screen_care/screen_filter.py +147 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/text/draw_text.py +12 -1
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/main_ui.py +16 -3
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/menu/preset_menu.py +21 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/menu/settings_menu.py +37 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/control_center/control_center_ui.py +27 -2
- frontengine-1.0.33/frontengine/ui/page/presentation/presentation_setting_ui.py +365 -0
- frontengine-1.0.33/frontengine/ui/page/screen_care/screen_care_setting_ui.py +295 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/user_setting/user_setting_file.py +3 -0
- frontengine-1.0.33/frontengine/utils/autostart/autostart_service.py +172 -0
- frontengine-1.0.33/frontengine/utils/break_reminder/break_reminder.py +104 -0
- frontengine-1.0.33/frontengine/utils/input_watch/input_watch_service.py +130 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/english.py +46 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/traditional_chinese.py +46 -0
- frontengine-1.0.33/frontengine/utils/platform_info/platform_info.py +303 -0
- frontengine-1.0.33/frontengine/utils/power_mode/__init__.py +0 -0
- frontengine-1.0.33/frontengine/utils/power_mode/power_mode.py +29 -0
- frontengine-1.0.33/frontengine/utils/preset_schedule/__init__.py +0 -0
- frontengine-1.0.33/frontengine/utils/redirect_manager/__init__.py +0 -0
- frontengine-1.0.33/frontengine/utils/smart_pause/__init__.py +0 -0
- frontengine-1.0.33/frontengine/utils/system_stats/__init__.py +0 -0
- frontengine-1.0.33/frontengine/utils/text_source/__init__.py +0 -0
- frontengine-1.0.33/frontengine/utils/theme_schedule/__init__.py +0 -0
- frontengine-1.0.33/frontengine/utils/weather/__init__.py +0 -0
- frontengine-1.0.33/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine.egg-info/PKG-INFO +1 -1
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine.egg-info/SOURCES.txt +21 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/pyproject.toml +1 -1
- {frontengine-1.0.31 → frontengine-1.0.33}/LICENSE +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/README.md +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/__main__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/base_widget.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/gif/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/image/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/load/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/particle/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/pet/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/show/scene → frontengine-1.0.33/frontengine/show/presentation}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/show/sound_player → frontengine-1.0.33/frontengine/show/scene}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.31/frontengine/show/text → frontengine-1.0.33/frontengine/show/screen_care}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/show/video → frontengine-1.0.33/frontengine/show/sound_player}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.31/frontengine/show/web → frontengine-1.0.33/frontengine/show/text}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/system_tray → frontengine-1.0.33/frontengine/show/video}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.31/frontengine/ui → frontengine-1.0.33/frontengine/show/web}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.31/frontengine/ui/color → frontengine-1.0.33/frontengine/system_tray}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.31/frontengine/ui/dialog → frontengine-1.0.33/frontengine/ui}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/ui/menu → frontengine-1.0.33/frontengine/ui/color}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page → frontengine-1.0.33/frontengine/ui/dialog}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/control_center → frontengine-1.0.33/frontengine/ui/menu}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/gif → frontengine-1.0.33/frontengine/ui/page}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/image → frontengine-1.0.33/frontengine/ui/page/control_center}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/particle → frontengine-1.0.33/frontengine/ui/page/gif}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/pet → frontengine-1.0.33/frontengine/ui/page/image}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/scene_setting → frontengine-1.0.33/frontengine/ui/page/particle}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.33/frontengine/ui/page/pet}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/sound_player → frontengine-1.0.33/frontengine/ui/page/presentation}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/text → frontengine-1.0.33/frontengine/ui/page/scene_setting}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/video → frontengine-1.0.33/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/web → frontengine-1.0.33/frontengine/ui/page/screen_care}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/user_setting → frontengine-1.0.33/frontengine/ui/page/sound_player}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/utils → frontengine-1.0.33/frontengine/ui/page/text}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.31/frontengine/utils/audio_meter → frontengine-1.0.33/frontengine/ui/page/video}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/utils/browser → frontengine-1.0.33/frontengine/ui/page/web}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/utils/critical_exit → frontengine-1.0.33/frontengine/user_setting}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.31/frontengine/utils/file → frontengine-1.0.33/frontengine/utils}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/utils/focus_timer → frontengine-1.0.33/frontengine/utils/audio_meter}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.31/frontengine/utils/hotkey → frontengine-1.0.33/frontengine/utils/autostart}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/utils/json → frontengine-1.0.33/frontengine/utils/break_reminder}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/utils/logging → frontengine-1.0.33/frontengine/utils/browser}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.31/frontengine/utils/multi_language → frontengine-1.0.33/frontengine/utils/critical_exit}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.31/frontengine/utils/pet_chat → frontengine-1.0.33/frontengine/utils/file}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.31/frontengine/utils/preset_schedule → frontengine-1.0.33/frontengine/utils/focus_timer}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/focus_timer/focus_timer.py +0 -0
- {frontengine-1.0.31/frontengine/utils/redirect_manager → frontengine-1.0.33/frontengine/utils/hotkey}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.31/frontengine/utils/smart_pause → frontengine-1.0.33/frontengine/utils/input_watch}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/utils/system_stats → frontengine-1.0.33/frontengine/utils/json}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.31/frontengine/utils/text_source → frontengine-1.0.33/frontengine/utils/logging}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.31/frontengine/utils/theme_schedule → frontengine-1.0.33/frontengine/utils/multi_language}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.31/frontengine/utils/weather → frontengine-1.0.33/frontengine/utils/pet_chat}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
- {frontengine-1.0.31/frontengine/worker → frontengine-1.0.33/frontengine/utils/platform_info}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/system_stats/system_stats.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/text_source/text_source.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/weather/weather_service.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.33}/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.33
|
|
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
|
|
@@ -16,6 +16,12 @@ from frontengine.show.window_helpers import apply_overlay_window_flags
|
|
|
16
16
|
from frontengine.user_setting.user_setting_file import user_setting_dict
|
|
17
17
|
from frontengine.utils.audio_meter.audio_envelope import AudioEnvelope
|
|
18
18
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
19
|
+
from frontengine.utils.power_mode.power_mode import scaled_interval
|
|
20
|
+
from frontengine.utils.platform_info.platform_info import (
|
|
21
|
+
idle_seconds as platform_idle_seconds,
|
|
22
|
+
read_battery as platform_read_battery,
|
|
23
|
+
standable_windows,
|
|
24
|
+
)
|
|
19
25
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
20
26
|
|
|
21
27
|
# 行為模式 / Behaviour modes
|
|
@@ -212,75 +218,17 @@ def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_sta
|
|
|
212
218
|
return STATE_WALK
|
|
213
219
|
|
|
214
220
|
|
|
215
|
-
# 列舉可站立視窗時要略過的外殼視窗類別
|
|
216
|
-
# Shell window classes skipped when enumerating standable windows.
|
|
217
|
-
_PLATFORM_SKIP_CLASSES = {"WorkerW", "Progman", "Shell_TrayWnd", "Button"}
|
|
218
|
-
|
|
219
|
-
|
|
220
221
|
def windows_platforms(exclude_hwnds=()) -> list:
|
|
221
222
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Return the top edges of visible top-level windows as standable platforms
|
|
225
|
-
(left, right, top_y). Windows only; returns [] elsewhere or on error.
|
|
223
|
+
回傳目前可站立的視窗上緣 (left, right, top_y);各平台實作見 platform_info。
|
|
224
|
+
Standable window top edges; per-platform implementations live in platform_info.
|
|
226
225
|
"""
|
|
227
|
-
|
|
228
|
-
return []
|
|
229
|
-
try:
|
|
230
|
-
import ctypes
|
|
231
|
-
from ctypes import wintypes
|
|
232
|
-
|
|
233
|
-
user32 = ctypes.windll.user32
|
|
234
|
-
exclude = {int(h) for h in exclude_hwnds}
|
|
235
|
-
platforms: list = []
|
|
236
|
-
|
|
237
|
-
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
238
|
-
def _collect(hwnd, _lparam):
|
|
239
|
-
if int(hwnd) in exclude or not user32.IsWindowVisible(hwnd):
|
|
240
|
-
return True
|
|
241
|
-
rect = wintypes.RECT()
|
|
242
|
-
if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
|
243
|
-
return True
|
|
244
|
-
width = rect.right - rect.left
|
|
245
|
-
height = rect.bottom - rect.top
|
|
246
|
-
if width < 80 or height < 40:
|
|
247
|
-
return True
|
|
248
|
-
class_buffer = ctypes.create_unicode_buffer(64)
|
|
249
|
-
user32.GetClassNameW(hwnd, class_buffer, 64)
|
|
250
|
-
if class_buffer.value in _PLATFORM_SKIP_CLASSES:
|
|
251
|
-
return True
|
|
252
|
-
platforms.append((rect.left, rect.right, rect.top))
|
|
253
|
-
return True
|
|
254
|
-
|
|
255
|
-
user32.EnumWindows(_collect, 0)
|
|
256
|
-
return platforms
|
|
257
|
-
except Exception as error: # pragma: no cover - Win32 boundary
|
|
258
|
-
front_engine_logger.warning(f"[windows_platforms] error: {error!r}")
|
|
259
|
-
return []
|
|
226
|
+
return standable_windows(exclude_hwnds)
|
|
260
227
|
|
|
261
228
|
|
|
262
229
|
def idle_seconds():
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
Seconds since the last user input, or None when unavailable (Windows only).
|
|
266
|
-
"""
|
|
267
|
-
if sys.platform != "win32":
|
|
268
|
-
return None
|
|
269
|
-
try:
|
|
270
|
-
import ctypes
|
|
271
|
-
|
|
272
|
-
class _LASTINPUTINFO(ctypes.Structure):
|
|
273
|
-
_fields_ = [("cbSize", ctypes.c_uint), ("dwTime", ctypes.c_uint)]
|
|
274
|
-
|
|
275
|
-
info = _LASTINPUTINFO()
|
|
276
|
-
info.cbSize = ctypes.sizeof(info)
|
|
277
|
-
if not ctypes.windll.user32.GetLastInputInfo(ctypes.byref(info)):
|
|
278
|
-
return None
|
|
279
|
-
elapsed_ms = ctypes.windll.kernel32.GetTickCount() - info.dwTime
|
|
280
|
-
return max(0.0, elapsed_ms / 1000.0)
|
|
281
|
-
except Exception as error: # pragma: no cover - Win32 boundary
|
|
282
|
-
front_engine_logger.warning(f"[idle_seconds] error: {error!r}")
|
|
283
|
-
return None
|
|
230
|
+
"""使用者未操作的秒數;取不到回傳 None(跨平台,見 platform_info)。"""
|
|
231
|
+
return platform_idle_seconds()
|
|
284
232
|
|
|
285
233
|
|
|
286
234
|
class PetTimeline:
|
|
@@ -335,35 +283,8 @@ def pop_due_reminders(now, reminders):
|
|
|
335
283
|
|
|
336
284
|
|
|
337
285
|
def read_battery():
|
|
338
|
-
"""
|
|
339
|
-
|
|
340
|
-
Return (percent, charging) or None when unavailable. Windows only.
|
|
341
|
-
"""
|
|
342
|
-
if sys.platform != "win32":
|
|
343
|
-
return None
|
|
344
|
-
try:
|
|
345
|
-
import ctypes
|
|
346
|
-
|
|
347
|
-
class _SPS(ctypes.Structure):
|
|
348
|
-
_fields_ = [
|
|
349
|
-
("ACLineStatus", ctypes.c_byte),
|
|
350
|
-
("BatteryFlag", ctypes.c_byte),
|
|
351
|
-
("BatteryLifePercent", ctypes.c_byte),
|
|
352
|
-
("SystemStatusFlag", ctypes.c_byte),
|
|
353
|
-
("BatteryLifeTime", ctypes.c_ulong),
|
|
354
|
-
("BatteryFullLifeTime", ctypes.c_ulong),
|
|
355
|
-
]
|
|
356
|
-
|
|
357
|
-
status = _SPS()
|
|
358
|
-
if not ctypes.windll.kernel32.GetSystemPowerStatus(ctypes.byref(status)):
|
|
359
|
-
return None
|
|
360
|
-
percent = status.BatteryLifePercent & 0xFF
|
|
361
|
-
if percent == 255: # unknown / no battery
|
|
362
|
-
return None
|
|
363
|
-
return (int(percent), status.ACLineStatus == 1)
|
|
364
|
-
except Exception as error: # pragma: no cover - Win32 boundary
|
|
365
|
-
front_engine_logger.warning(f"[read_battery] error: {error!r}")
|
|
366
|
-
return None
|
|
286
|
+
"""回傳 (電量百分比, 是否充電中);沒有電池或取不到回傳 None(跨平台)。"""
|
|
287
|
+
return platform_read_battery()
|
|
367
288
|
|
|
368
289
|
|
|
369
290
|
def nearest_peer(center, peers):
|
|
@@ -1147,6 +1068,9 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1147
1068
|
|
|
1148
1069
|
self._timer = QTimer(self)
|
|
1149
1070
|
self._timer.timeout.connect(self._on_tick)
|
|
1071
|
+
# 省電模式與原始更新間隔 / Low-power state and the base tick interval
|
|
1072
|
+
self._low_power = False
|
|
1073
|
+
self._tick_interval_ms = 33
|
|
1150
1074
|
|
|
1151
1075
|
# Speech bubbles / chatter / mood
|
|
1152
1076
|
self._talk = bool(talk)
|
|
@@ -1417,6 +1341,12 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1417
1341
|
painter.drawPixmap(
|
|
1418
1342
|
QRect((self.width() - width) // 2, self.height() - height, width, height), pixmap)
|
|
1419
1343
|
|
|
1344
|
+
def set_low_power(self, enabled: bool) -> None:
|
|
1345
|
+
"""省電模式:把移動更新拉慢(畫面較不順,但省電)。"""
|
|
1346
|
+
self._low_power = bool(enabled)
|
|
1347
|
+
if self._timer.isActive():
|
|
1348
|
+
self._timer.start(scaled_interval(self._tick_interval_ms, self._low_power))
|
|
1349
|
+
|
|
1420
1350
|
def start_moving(self, bounds: Tuple[int, int, int, int], interval_ms: int = 33) -> None:
|
|
1421
1351
|
front_engine_logger.info(f"[DesktopPetWidget] start_moving | bounds={bounds}")
|
|
1422
1352
|
left, top, right, bottom = bounds
|
|
@@ -1424,7 +1354,8 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1424
1354
|
self.motion.x = float(left)
|
|
1425
1355
|
self.motion.y = float(bottom - self.pet_size)
|
|
1426
1356
|
self.move(int(self.motion.x), int(self.motion.y))
|
|
1427
|
-
self.
|
|
1357
|
+
self._tick_interval_ms = max(10, int(interval_ms))
|
|
1358
|
+
self._timer.start(scaled_interval(self._tick_interval_ms, self._low_power))
|
|
1428
1359
|
self._schedule_chatter()
|
|
1429
1360
|
self._reminder_timer.start(10000)
|
|
1430
1361
|
if self._timeline_start is None:
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
螢幕塗鴉層:在畫面上用筆或螢光筆畫線、清除、復原。畫圖時需要接收滑鼠,
|
|
3
|
+
所以它不是點擊穿透的;關閉塗鴉即恢復原本的操作。
|
|
4
|
+
|
|
5
|
+
A screen annotation layer: draw over the screen with a pen or highlighter,
|
|
6
|
+
undo, and clear. It has to take the mouse while you draw, so unlike the other
|
|
7
|
+
overlays it is not click-through — turn it off and the screen is yours again.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import QPoint, Qt
|
|
14
|
+
from PySide6.QtGui import QColor, QPainter, QPen
|
|
15
|
+
|
|
16
|
+
from frontengine.show.base_widget import BaseWidget
|
|
17
|
+
from frontengine.show.window_helpers import apply_overlay_window_flags
|
|
18
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
19
|
+
|
|
20
|
+
TOOL_PEN = "pen"
|
|
21
|
+
TOOL_HIGHLIGHTER = "highlighter"
|
|
22
|
+
TOOL_ERASER = "eraser"
|
|
23
|
+
TOOLS = (TOOL_PEN, TOOL_HIGHLIGHTER, TOOL_ERASER)
|
|
24
|
+
|
|
25
|
+
PEN_COLORS = (
|
|
26
|
+
("Red", "#ff3b30"), ("Yellow", "#ffcc00"), ("Green", "#34c759"),
|
|
27
|
+
("Blue", "#0a84ff"), ("Black", "#000000"), ("White", "#ffffff"),
|
|
28
|
+
)
|
|
29
|
+
DEFAULT_PEN_COLOR = "#ff3b30"
|
|
30
|
+
MIN_WIDTH = 1
|
|
31
|
+
MAX_WIDTH = 40
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def clamp_width(value, fallback: int = 4) -> int:
|
|
35
|
+
"""把筆寬夾在可用範圍。"""
|
|
36
|
+
try:
|
|
37
|
+
return max(MIN_WIDTH, min(MAX_WIDTH, int(value)))
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return fallback
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def stroke_style(tool: str, color: str, width: int) -> Tuple[QColor, int]:
|
|
43
|
+
"""
|
|
44
|
+
依工具決定實際的顏色與線寬:螢光筆是半透明的粗線,橡皮擦畫得更粗。
|
|
45
|
+
Per-tool colour and width: a highlighter is a wide translucent stroke, an
|
|
46
|
+
eraser is wider still.
|
|
47
|
+
"""
|
|
48
|
+
pen_color = QColor(color)
|
|
49
|
+
if not pen_color.isValid():
|
|
50
|
+
pen_color = QColor(DEFAULT_PEN_COLOR)
|
|
51
|
+
stroke_width = clamp_width(width)
|
|
52
|
+
if tool == TOOL_HIGHLIGHTER:
|
|
53
|
+
pen_color.setAlpha(90)
|
|
54
|
+
stroke_width = clamp_width(stroke_width * 4)
|
|
55
|
+
elif tool == TOOL_ERASER:
|
|
56
|
+
stroke_width = clamp_width(stroke_width * 5)
|
|
57
|
+
return (pen_color, stroke_width)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AnnotationOverlay(BaseWidget):
|
|
61
|
+
"""
|
|
62
|
+
塗鴉層:滑鼠拖曳畫線,支援筆/螢光筆/橡皮擦、復原與全部清除。
|
|
63
|
+
每一筆記成 (工具, 顏色, 線寬, 點清單),因此復原與重繪都很單純。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, color: str = DEFAULT_PEN_COLOR, width: int = 4, tool: str = TOOL_PEN) -> None:
|
|
67
|
+
front_engine_logger.info(f"[AnnotationOverlay] Init | tool={tool}, color={color}, width={width}")
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.opacity = 1.0
|
|
70
|
+
self.tool = tool if tool in TOOLS else TOOL_PEN
|
|
71
|
+
self.pen_color = color
|
|
72
|
+
self.pen_width = clamp_width(width)
|
|
73
|
+
self.strokes: List[dict] = []
|
|
74
|
+
self._current: Optional[dict] = None
|
|
75
|
+
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
76
|
+
|
|
77
|
+
def set_annotation_window_flag(self) -> None:
|
|
78
|
+
"""塗鴉需要接收滑鼠,所以不使用點擊穿透。"""
|
|
79
|
+
apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
|
|
80
|
+
|
|
81
|
+
def set_tool(self, tool: str) -> None:
|
|
82
|
+
if tool in TOOLS:
|
|
83
|
+
self.tool = tool
|
|
84
|
+
|
|
85
|
+
def set_pen(self, color: Optional[str] = None, width: Optional[int] = None) -> None:
|
|
86
|
+
if color is not None:
|
|
87
|
+
self.pen_color = color
|
|
88
|
+
if width is not None:
|
|
89
|
+
self.pen_width = clamp_width(width)
|
|
90
|
+
|
|
91
|
+
def clear(self) -> None:
|
|
92
|
+
"""清掉所有筆畫 / Wipe every stroke."""
|
|
93
|
+
self.strokes = []
|
|
94
|
+
self._current = None
|
|
95
|
+
self.update()
|
|
96
|
+
|
|
97
|
+
def undo(self) -> bool:
|
|
98
|
+
"""收回最後一筆;沒有筆畫可收時回傳 False。"""
|
|
99
|
+
if not self.strokes:
|
|
100
|
+
return False
|
|
101
|
+
self.strokes.pop()
|
|
102
|
+
self.update()
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
# --- drawing ---------------------------------------------------------
|
|
106
|
+
def begin_stroke(self, point: QPoint) -> None:
|
|
107
|
+
self._current = {
|
|
108
|
+
"tool": self.tool, "color": self.pen_color, "width": self.pen_width, "points": [point],
|
|
109
|
+
}
|
|
110
|
+
self.strokes.append(self._current)
|
|
111
|
+
|
|
112
|
+
def extend_stroke(self, point: QPoint) -> None:
|
|
113
|
+
if self._current is not None:
|
|
114
|
+
self._current["points"].append(point)
|
|
115
|
+
self.update()
|
|
116
|
+
|
|
117
|
+
def end_stroke(self) -> None:
|
|
118
|
+
# 只有一個點的筆畫畫不出線,直接丟掉
|
|
119
|
+
# A single-point stroke draws nothing; drop it.
|
|
120
|
+
if self._current is not None and len(self._current["points"]) < 2:
|
|
121
|
+
self.strokes.remove(self._current)
|
|
122
|
+
self.update()
|
|
123
|
+
self._current = None
|
|
124
|
+
|
|
125
|
+
def mousePressEvent(self, event) -> None:
|
|
126
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
127
|
+
self.begin_stroke(event.position().toPoint())
|
|
128
|
+
super().mousePressEvent(event)
|
|
129
|
+
|
|
130
|
+
def mouseMoveEvent(self, event) -> None:
|
|
131
|
+
self.extend_stroke(event.position().toPoint())
|
|
132
|
+
super().mouseMoveEvent(event)
|
|
133
|
+
|
|
134
|
+
def mouseReleaseEvent(self, event) -> None:
|
|
135
|
+
self.end_stroke()
|
|
136
|
+
super().mouseReleaseEvent(event)
|
|
137
|
+
|
|
138
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
139
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
140
|
+
for stroke in self.strokes:
|
|
141
|
+
points = stroke["points"]
|
|
142
|
+
if len(points) < 2:
|
|
143
|
+
continue
|
|
144
|
+
color, width = stroke_style(stroke["tool"], stroke["color"], stroke["width"])
|
|
145
|
+
if stroke["tool"] == TOOL_ERASER:
|
|
146
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
|
|
147
|
+
else:
|
|
148
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
|
|
149
|
+
painter.setPen(QPen(color, width, Qt.PenStyle.SolidLine,
|
|
150
|
+
Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
|
|
151
|
+
for start, end in zip(points, points[1:]):
|
|
152
|
+
painter.drawLine(start, end)
|
|
153
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
游標強調層:在游標周圍畫一圈亮環、點擊時擴散漣漪,或把游標以外的區域壓暗
|
|
3
|
+
(聚光燈)。教學、簡報與錄影時讓觀眾看得見滑鼠在哪。點擊穿透,不影響操作。
|
|
4
|
+
|
|
5
|
+
Cursor emphasis: a ring around the pointer, a ripple when you click, or a
|
|
6
|
+
spotlight that dims everything except a circle around the cursor — so an
|
|
7
|
+
audience can follow the mouse during a demo. Click-through throughout.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import QPoint, QTimer, Qt
|
|
14
|
+
from PySide6.QtGui import QColor, QCursor, QPainter, QRadialGradient
|
|
15
|
+
|
|
16
|
+
from frontengine.show.base_widget import BaseWidget
|
|
17
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
18
|
+
|
|
19
|
+
DEFAULT_RING_COLOR = "#ffcc00"
|
|
20
|
+
DEFAULT_RING_RADIUS = 28
|
|
21
|
+
DEFAULT_SPOTLIGHT_RADIUS = 160
|
|
22
|
+
RIPPLE_STEPS = 12
|
|
23
|
+
_MIN_RADIUS = 8
|
|
24
|
+
_MAX_RADIUS = 600
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clamp_radius(value, fallback: int = DEFAULT_RING_RADIUS) -> int:
|
|
28
|
+
"""把半徑夾在看得見又不至於蓋滿螢幕的範圍。"""
|
|
29
|
+
try:
|
|
30
|
+
return max(_MIN_RADIUS, min(_MAX_RADIUS, int(value)))
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
return fallback
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ripple_radius(step: int, base_radius: int, steps: int = RIPPLE_STEPS) -> int:
|
|
36
|
+
"""漣漪第 step 幀的半徑:從基準半徑往外擴散一倍。"""
|
|
37
|
+
total = max(1, int(steps))
|
|
38
|
+
progress = min(max(int(step), 0), total) / total
|
|
39
|
+
return int(clamp_radius(base_radius) * (1.0 + progress))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ripple_alpha(step: int, steps: int = RIPPLE_STEPS) -> int:
|
|
43
|
+
"""漣漪第 step 幀的透明度:越擴散越淡,最後消失。"""
|
|
44
|
+
total = max(1, int(steps))
|
|
45
|
+
progress = min(max(int(step), 0), total) / total
|
|
46
|
+
return max(0, int(200 * (1.0 - progress)))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CursorEffectWidget(BaseWidget):
|
|
50
|
+
"""
|
|
51
|
+
游標效果層:亮環、點擊漣漪、聚光燈可各自開關,全部跟著游標移動。
|
|
52
|
+
位置與點擊事件由外部注入(正式路徑接系統游標與全域滑鼠監聽),方便測試。
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
REFRESH_MS = 16
|
|
56
|
+
|
|
57
|
+
def __init__(self, ring: bool = True, ripple: bool = True, spotlight: bool = False,
|
|
58
|
+
color: str = DEFAULT_RING_COLOR, radius: int = DEFAULT_RING_RADIUS,
|
|
59
|
+
spotlight_radius: int = DEFAULT_SPOTLIGHT_RADIUS, dim: int = 55) -> None:
|
|
60
|
+
front_engine_logger.info(
|
|
61
|
+
f"[CursorEffectWidget] Init | ring={ring}, ripple={ripple}, spotlight={spotlight}")
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.opacity = 1.0
|
|
64
|
+
self.show_ring = bool(ring)
|
|
65
|
+
self.show_ripple = bool(ripple)
|
|
66
|
+
self.show_spotlight = bool(spotlight)
|
|
67
|
+
self.color = QColor(color) if QColor(color).isValid() else QColor(DEFAULT_RING_COLOR)
|
|
68
|
+
self.radius = clamp_radius(radius)
|
|
69
|
+
self.spotlight_radius = clamp_radius(spotlight_radius, DEFAULT_SPOTLIGHT_RADIUS)
|
|
70
|
+
self.dim = max(0, min(90, int(dim)))
|
|
71
|
+
self.cursor_point = QPoint(0, 0)
|
|
72
|
+
self.ripples: List[dict] = []
|
|
73
|
+
self._cursor_provider = QCursor.pos
|
|
74
|
+
self._timer = QTimer(self)
|
|
75
|
+
self._timer.timeout.connect(self.tick)
|
|
76
|
+
|
|
77
|
+
def set_cursor_provider(self, provider) -> None:
|
|
78
|
+
"""注入游標位置來源(測試用)。"""
|
|
79
|
+
self._cursor_provider = provider
|
|
80
|
+
|
|
81
|
+
def set_effects(self, ring: Optional[bool] = None, ripple: Optional[bool] = None,
|
|
82
|
+
spotlight: Optional[bool] = None) -> None:
|
|
83
|
+
"""開關個別效果。"""
|
|
84
|
+
if ring is not None:
|
|
85
|
+
self.show_ring = bool(ring)
|
|
86
|
+
if ripple is not None:
|
|
87
|
+
self.show_ripple = bool(ripple)
|
|
88
|
+
if spotlight is not None:
|
|
89
|
+
self.show_spotlight = bool(spotlight)
|
|
90
|
+
self.update()
|
|
91
|
+
|
|
92
|
+
def set_style(self, color: Optional[str] = None, radius: Optional[int] = None,
|
|
93
|
+
spotlight_radius: Optional[int] = None, dim: Optional[int] = None) -> None:
|
|
94
|
+
"""調整顏色、半徑與壓暗程度。"""
|
|
95
|
+
if color is not None:
|
|
96
|
+
candidate = QColor(color)
|
|
97
|
+
if candidate.isValid():
|
|
98
|
+
self.color = candidate
|
|
99
|
+
if radius is not None:
|
|
100
|
+
self.radius = clamp_radius(radius)
|
|
101
|
+
if spotlight_radius is not None:
|
|
102
|
+
self.spotlight_radius = clamp_radius(spotlight_radius, DEFAULT_SPOTLIGHT_RADIUS)
|
|
103
|
+
if dim is not None:
|
|
104
|
+
self.dim = max(0, min(90, int(dim)))
|
|
105
|
+
self.update()
|
|
106
|
+
|
|
107
|
+
def start_following(self, interval_ms: int = REFRESH_MS) -> None:
|
|
108
|
+
"""開始跟隨游標。"""
|
|
109
|
+
self.tick()
|
|
110
|
+
self._timer.start(max(8, int(interval_ms)))
|
|
111
|
+
|
|
112
|
+
def add_ripple(self, point: Optional[QPoint] = None) -> None:
|
|
113
|
+
"""在指定位置(預設為目前游標)加一圈點擊漣漪。"""
|
|
114
|
+
self.ripples.append({"point": QPoint(point or self.cursor_point), "step": 0})
|
|
115
|
+
self.update()
|
|
116
|
+
|
|
117
|
+
def tick(self) -> None:
|
|
118
|
+
"""更新游標位置並推進漣漪動畫。"""
|
|
119
|
+
try:
|
|
120
|
+
position = self._cursor_provider()
|
|
121
|
+
except Exception: # pragma: no cover - defensive around injected providers
|
|
122
|
+
return
|
|
123
|
+
self.cursor_point = QPoint(int(position.x()) - self.x(), int(position.y()) - self.y())
|
|
124
|
+
for ripple in list(self.ripples):
|
|
125
|
+
ripple["step"] += 1
|
|
126
|
+
if ripple["step"] > RIPPLE_STEPS:
|
|
127
|
+
self.ripples.remove(ripple)
|
|
128
|
+
self.update()
|
|
129
|
+
|
|
130
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
131
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
132
|
+
if self.show_spotlight:
|
|
133
|
+
self._draw_spotlight(painter)
|
|
134
|
+
if self.show_ring:
|
|
135
|
+
ring_color = QColor(self.color)
|
|
136
|
+
ring_color.setAlpha(120)
|
|
137
|
+
painter.setBrush(ring_color)
|
|
138
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
139
|
+
painter.drawEllipse(self.cursor_point, self.radius, self.radius)
|
|
140
|
+
if self.show_ripple:
|
|
141
|
+
for ripple in self.ripples:
|
|
142
|
+
color = QColor(self.color)
|
|
143
|
+
color.setAlpha(ripple_alpha(ripple["step"]))
|
|
144
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
145
|
+
painter.setPen(color)
|
|
146
|
+
radius = ripple_radius(ripple["step"], self.radius)
|
|
147
|
+
painter.drawEllipse(ripple["point"], radius, radius)
|
|
148
|
+
|
|
149
|
+
def _draw_spotlight(self, painter: QPainter) -> None:
|
|
150
|
+
"""把游標以外壓暗,中心保持透明,邊緣柔和過渡。"""
|
|
151
|
+
shade = QColor(0, 0, 0, int(255 * self.dim / 100.0))
|
|
152
|
+
gradient = QRadialGradient(self.cursor_point, self.spotlight_radius)
|
|
153
|
+
gradient.setColorAt(0.0, QColor(0, 0, 0, 0))
|
|
154
|
+
gradient.setColorAt(0.75, QColor(0, 0, 0, 0))
|
|
155
|
+
gradient.setColorAt(1.0, shade)
|
|
156
|
+
painter.fillRect(self.rect(), shade)
|
|
157
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
|
|
158
|
+
painter.setBrush(gradient)
|
|
159
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
160
|
+
painter.drawEllipse(self.cursor_point, self.spotlight_radius, self.spotlight_radius)
|
|
161
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
|
|
162
|
+
|
|
163
|
+
def closeEvent(self, event) -> None:
|
|
164
|
+
if self._timer.isActive():
|
|
165
|
+
self._timer.stop()
|
|
166
|
+
super().closeEvent(event)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
按鍵顯示:把剛按下的按鍵顯示在畫面角落,做教學影片或直播時觀眾才知道你按了什麼。
|
|
3
|
+
PowerToys 至今沒有內建這個功能,而本專案本來就有全域鍵盤監聽(pynput),
|
|
4
|
+
所以只需要一層顯示。按鍵格式化是純函式,可獨立測試。
|
|
5
|
+
|
|
6
|
+
Keystroke display: show what you just pressed in a screen corner, so viewers of
|
|
7
|
+
a tutorial or stream can follow along. PowerToys still has no built-in version;
|
|
8
|
+
this project already listens for global hotkeys, so only the display is new.
|
|
9
|
+
The formatting is pure and independently testable.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from typing import List, Tuple
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import QRect, QTimer, Qt
|
|
17
|
+
from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter
|
|
18
|
+
|
|
19
|
+
from frontengine.show.base_widget import BaseWidget
|
|
20
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
21
|
+
|
|
22
|
+
DEFAULT_HOLD_SECONDS = 2.0
|
|
23
|
+
MAX_KEYS_SHOWN = 6
|
|
24
|
+
|
|
25
|
+
# 修飾鍵的顯示名稱 / How modifier keys are shown
|
|
26
|
+
_MODIFIER_NAMES = {
|
|
27
|
+
"ctrl": "Ctrl", "ctrl_l": "Ctrl", "ctrl_r": "Ctrl",
|
|
28
|
+
"alt": "Alt", "alt_l": "Alt", "alt_r": "Alt", "alt_gr": "AltGr",
|
|
29
|
+
"shift": "Shift", "shift_l": "Shift", "shift_r": "Shift",
|
|
30
|
+
"cmd": "Win", "cmd_l": "Win", "cmd_r": "Win",
|
|
31
|
+
}
|
|
32
|
+
# 特殊鍵的顯示名稱 / Friendlier names for special keys
|
|
33
|
+
_SPECIAL_NAMES = {
|
|
34
|
+
"space": "Space", "enter": "Enter", "return": "Enter", "tab": "Tab",
|
|
35
|
+
"backspace": "Backspace", "delete": "Del", "esc": "Esc", "escape": "Esc",
|
|
36
|
+
"up": "↑", "down": "↓", "left": "←", "right": "→",
|
|
37
|
+
"page_up": "PgUp", "page_down": "PgDn", "home": "Home", "end": "End",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_key(name) -> str:
|
|
42
|
+
"""
|
|
43
|
+
把按鍵名稱轉成畫面上好讀的字(修飾鍵、方向鍵、功能鍵各有寫法)。
|
|
44
|
+
Turn a key name into something readable on screen.
|
|
45
|
+
"""
|
|
46
|
+
text = str(name or "").strip().strip("'")
|
|
47
|
+
if not text:
|
|
48
|
+
return ""
|
|
49
|
+
lowered = text.lower()
|
|
50
|
+
if lowered in _MODIFIER_NAMES:
|
|
51
|
+
return _MODIFIER_NAMES[lowered]
|
|
52
|
+
if lowered in _SPECIAL_NAMES:
|
|
53
|
+
return _SPECIAL_NAMES[lowered]
|
|
54
|
+
if lowered.startswith("key."):
|
|
55
|
+
return format_key(text[4:])
|
|
56
|
+
if len(text) == 1:
|
|
57
|
+
return text.upper()
|
|
58
|
+
if lowered.startswith("f") and lowered[1:].isdigit():
|
|
59
|
+
return text.upper()
|
|
60
|
+
return text.replace("_", " ").title()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def format_combo(keys) -> str:
|
|
64
|
+
"""把同時按著的按鍵組成 `Ctrl + Shift + S` 這種字串(去重、保留順序)。"""
|
|
65
|
+
parts: List[str] = []
|
|
66
|
+
for key in keys or ():
|
|
67
|
+
formatted = format_key(key)
|
|
68
|
+
if formatted and formatted not in parts:
|
|
69
|
+
parts.append(formatted)
|
|
70
|
+
return " + ".join(parts)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def visible_keys(entries, now: float, hold_seconds: float = DEFAULT_HOLD_SECONDS,
|
|
74
|
+
limit: int = MAX_KEYS_SHOWN) -> List[str]:
|
|
75
|
+
"""
|
|
76
|
+
從 (文字, 時間) 清單裡挑出還沒過期的按鍵,最多顯示 limit 個(最新在後)。
|
|
77
|
+
The still-fresh entries from a list of (text, timestamp) pairs.
|
|
78
|
+
"""
|
|
79
|
+
fresh = [text for text, stamp in entries or () if now - stamp <= max(0.1, float(hold_seconds))]
|
|
80
|
+
return fresh[-max(1, int(limit)):]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class KeystrokeDisplayWidget(BaseWidget):
|
|
84
|
+
"""
|
|
85
|
+
按鍵顯示層:把最近按下的按鍵排成一列顯示,過幾秒自動淡出。點擊穿透。
|
|
86
|
+
按鍵來源由外部推入(`push_keys`),因此不需要在這裡自己開監聽器。
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
REFRESH_MS = 100
|
|
90
|
+
PADDING = 12
|
|
91
|
+
|
|
92
|
+
def __init__(self, hold_seconds: float = DEFAULT_HOLD_SECONDS, font_size: int = 28,
|
|
93
|
+
color: str = "#ffffff", background: str = "#000000") -> None:
|
|
94
|
+
front_engine_logger.info(f"[KeystrokeDisplayWidget] Init | hold={hold_seconds}s")
|
|
95
|
+
super().__init__()
|
|
96
|
+
self.opacity = 1.0
|
|
97
|
+
self.hold_seconds = max(0.2, float(hold_seconds))
|
|
98
|
+
self.text_color = QColor(color) if QColor(color).isValid() else QColor("#ffffff")
|
|
99
|
+
self.background_color = QColor(background) if QColor(background).isValid() else QColor("#000000")
|
|
100
|
+
self.font_size = max(8, int(font_size))
|
|
101
|
+
self.entries: List[Tuple[str, float]] = []
|
|
102
|
+
self._now = time.monotonic
|
|
103
|
+
self._timer = QTimer(self)
|
|
104
|
+
self._timer.timeout.connect(self._expire)
|
|
105
|
+
|
|
106
|
+
def set_clock(self, clock) -> None:
|
|
107
|
+
"""注入時間來源(測試用)。"""
|
|
108
|
+
self._now = clock
|
|
109
|
+
|
|
110
|
+
def start(self, interval_ms: int = REFRESH_MS) -> None:
|
|
111
|
+
"""開始定期清掉過期的按鍵。"""
|
|
112
|
+
self._timer.start(max(30, int(interval_ms)))
|
|
113
|
+
|
|
114
|
+
def push_keys(self, keys) -> None:
|
|
115
|
+
"""
|
|
116
|
+
推入一次按鍵事件(單一鍵或組合鍵);空的輸入忽略。
|
|
117
|
+
Record one keypress (single key or combination); blanks are ignored.
|
|
118
|
+
"""
|
|
119
|
+
text = format_combo(keys if isinstance(keys, (list, tuple, set)) else [keys])
|
|
120
|
+
if not text:
|
|
121
|
+
return
|
|
122
|
+
self.entries.append((text, self._now()))
|
|
123
|
+
self.entries = self.entries[-(MAX_KEYS_SHOWN * 2):]
|
|
124
|
+
self.update()
|
|
125
|
+
|
|
126
|
+
def current_text(self) -> str:
|
|
127
|
+
"""目前該顯示的字串。"""
|
|
128
|
+
return " ".join(visible_keys(self.entries, self._now(), self.hold_seconds))
|
|
129
|
+
|
|
130
|
+
def _expire(self) -> None:
|
|
131
|
+
before = len(visible_keys(self.entries, self._now(), self.hold_seconds))
|
|
132
|
+
if before != len(self.entries):
|
|
133
|
+
self.entries = [
|
|
134
|
+
(text, stamp) for text, stamp in self.entries
|
|
135
|
+
if self._now() - stamp <= self.hold_seconds
|
|
136
|
+
]
|
|
137
|
+
self.update()
|
|
138
|
+
|
|
139
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
140
|
+
text = self.current_text()
|
|
141
|
+
if not text:
|
|
142
|
+
return
|
|
143
|
+
font = QFont(self.font().family(), self.font_size)
|
|
144
|
+
painter.setFont(font)
|
|
145
|
+
metrics = QFontMetrics(font)
|
|
146
|
+
width = metrics.horizontalAdvance(text) + self.PADDING * 2
|
|
147
|
+
height = metrics.height() + self.PADDING
|
|
148
|
+
box = QRect(max(0, (self.width() - width) // 2),
|
|
149
|
+
max(0, self.height() - height - self.PADDING), width, height)
|
|
150
|
+
background = QColor(self.background_color)
|
|
151
|
+
background.setAlpha(170)
|
|
152
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
153
|
+
painter.setBrush(background)
|
|
154
|
+
painter.drawRoundedRect(box, 8, 8)
|
|
155
|
+
painter.setPen(self.text_color)
|
|
156
|
+
painter.drawText(box, int(Qt.AlignmentFlag.AlignCenter), text)
|
|
157
|
+
|
|
158
|
+
def closeEvent(self, event) -> None:
|
|
159
|
+
if self._timer.isActive():
|
|
160
|
+
self._timer.stop()
|
|
161
|
+
super().closeEvent(event)
|