frontengine 1.0.31__tar.gz → 1.0.32__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.31 → frontengine-1.0.32}/PKG-INFO +1 -1
- frontengine-1.0.32/frontengine/show/presentation/annotation_overlay.py +153 -0
- frontengine-1.0.32/frontengine/show/presentation/cursor_effects.py +166 -0
- frontengine-1.0.32/frontengine/show/presentation/keystroke_display.py +161 -0
- frontengine-1.0.32/frontengine/show/presentation/magnifier.py +127 -0
- frontengine-1.0.32/frontengine/show/screen_care/screen_filter.py +147 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/main_ui.py +6 -0
- frontengine-1.0.32/frontengine/ui/page/presentation/presentation_setting_ui.py +365 -0
- frontengine-1.0.32/frontengine/ui/page/screen_care/screen_care_setting_ui.py +295 -0
- frontengine-1.0.32/frontengine/utils/break_reminder/break_reminder.py +104 -0
- frontengine-1.0.32/frontengine/utils/input_watch/input_watch_service.py +130 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/english.py +41 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/traditional_chinese.py +41 -0
- frontengine-1.0.32/frontengine/utils/smart_pause/__init__.py +0 -0
- frontengine-1.0.32/frontengine/utils/system_stats/__init__.py +0 -0
- frontengine-1.0.32/frontengine/utils/text_source/__init__.py +0 -0
- frontengine-1.0.32/frontengine/utils/theme_schedule/__init__.py +0 -0
- frontengine-1.0.32/frontengine/utils/weather/__init__.py +0 -0
- frontengine-1.0.32/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/PKG-INFO +1 -1
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/SOURCES.txt +15 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/pyproject.toml +1 -1
- {frontengine-1.0.31 → frontengine-1.0.32}/LICENSE +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/README.md +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/__main__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/base_widget.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/gif/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/image/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/load/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/particle/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/pet/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/pet/desktop_pet.py +0 -0
- {frontengine-1.0.31/frontengine/show/scene → frontengine-1.0.32/frontengine/show/presentation}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/show/sound_player → frontengine-1.0.32/frontengine/show/scene}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.31/frontengine/show/text → frontengine-1.0.32/frontengine/show/screen_care}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/show/video → frontengine-1.0.32/frontengine/show/sound_player}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.31/frontengine/show/web → frontengine-1.0.32/frontengine/show/text}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/text/draw_text.py +0 -0
- {frontengine-1.0.31/frontengine/system_tray → frontengine-1.0.32/frontengine/show/video}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.31/frontengine/ui → frontengine-1.0.32/frontengine/show/web}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.31/frontengine/ui/color → frontengine-1.0.32/frontengine/system_tray}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.31/frontengine/ui/dialog → frontengine-1.0.32/frontengine/ui}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/ui/menu → frontengine-1.0.32/frontengine/ui/color}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page → frontengine-1.0.32/frontengine/ui/dialog}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/control_center → frontengine-1.0.32/frontengine/ui/menu}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/menu/settings_menu.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/gif → frontengine-1.0.32/frontengine/ui/page}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/image → frontengine-1.0.32/frontengine/ui/page/control_center}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/particle → frontengine-1.0.32/frontengine/ui/page/gif}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/pet → frontengine-1.0.32/frontengine/ui/page/image}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/scene_setting → frontengine-1.0.32/frontengine/ui/page/particle}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.32/frontengine/ui/page/pet}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/sound_player → frontengine-1.0.32/frontengine/ui/page/presentation}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/text → frontengine-1.0.32/frontengine/ui/page/scene_setting}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/video → frontengine-1.0.32/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/ui/page/web → frontengine-1.0.32/frontengine/ui/page/screen_care}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/user_setting → frontengine-1.0.32/frontengine/ui/page/sound_player}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/utils → frontengine-1.0.32/frontengine/ui/page/text}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.31/frontengine/utils/audio_meter → frontengine-1.0.32/frontengine/ui/page/video}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/utils/browser → frontengine-1.0.32/frontengine/ui/page/web}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.31/frontengine/utils/critical_exit → frontengine-1.0.32/frontengine/user_setting}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/user_setting/user_setting_file.py +0 -0
- {frontengine-1.0.31/frontengine/utils/file → frontengine-1.0.32/frontengine/utils}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/utils/focus_timer → frontengine-1.0.32/frontengine/utils/audio_meter}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.31/frontengine/utils/hotkey → frontengine-1.0.32/frontengine/utils/break_reminder}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/utils/json → frontengine-1.0.32/frontengine/utils/browser}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.31/frontengine/utils/logging → frontengine-1.0.32/frontengine/utils/critical_exit}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.31/frontengine/utils/multi_language → frontengine-1.0.32/frontengine/utils/file}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.31/frontengine/utils/pet_chat → frontengine-1.0.32/frontengine/utils/focus_timer}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/focus_timer/focus_timer.py +0 -0
- {frontengine-1.0.31/frontengine/utils/preset_schedule → frontengine-1.0.32/frontengine/utils/hotkey}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.31/frontengine/utils/redirect_manager → frontengine-1.0.32/frontengine/utils/input_watch}/__init__.py +0 -0
- {frontengine-1.0.31/frontengine/utils/smart_pause → frontengine-1.0.32/frontengine/utils/json}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.31/frontengine/utils/system_stats → frontengine-1.0.32/frontengine/utils/logging}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.31/frontengine/utils/text_source → frontengine-1.0.32/frontengine/utils/multi_language}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.31/frontengine/utils/theme_schedule → frontengine-1.0.32/frontengine/utils/pet_chat}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
- {frontengine-1.0.31/frontengine/utils/weather → frontengine-1.0.32/frontengine/utils/preset_schedule}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.31/frontengine/worker → frontengine-1.0.32/frontengine/utils/redirect_manager}/__init__.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/system_stats/system_stats.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/text_source/text_source.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/weather/weather_service.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.31 → frontengine-1.0.32}/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.32
|
|
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,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)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
區域放大鏡:抓取游標周圍的一小塊畫面,放大後顯示在覆蓋層上,
|
|
3
|
+
方便簡報時展示細節(ZoomIt 式)。點擊穿透,抓圖用 Qt 自己的螢幕擷取。
|
|
4
|
+
|
|
5
|
+
A region magnifier: grab a small area around the cursor and draw it enlarged,
|
|
6
|
+
for showing detail during a demo. Click-through, and the capture uses Qt's own
|
|
7
|
+
screen grab rather than any platform API.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import QRect, QTimer, Qt
|
|
14
|
+
from PySide6.QtGui import QColor, QCursor, QGuiApplication, QPainter, QPixmap
|
|
15
|
+
|
|
16
|
+
from frontengine.show.base_widget import BaseWidget
|
|
17
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
18
|
+
|
|
19
|
+
MIN_ZOOM = 1.25
|
|
20
|
+
MAX_ZOOM = 8.0
|
|
21
|
+
DEFAULT_ZOOM = 2.0
|
|
22
|
+
DEFAULT_SIZE = 240
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def clamp_zoom(value, fallback: float = DEFAULT_ZOOM) -> float:
|
|
26
|
+
"""把放大倍率夾在 1.25x~8x(再高就只剩馬賽克)。"""
|
|
27
|
+
try:
|
|
28
|
+
return max(MIN_ZOOM, min(MAX_ZOOM, round(float(value), 2)))
|
|
29
|
+
except (TypeError, ValueError):
|
|
30
|
+
return fallback
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def source_rect(center: Tuple[int, int], size: int, zoom: float,
|
|
34
|
+
bounds: Tuple[int, int, int, int]) -> QRect:
|
|
35
|
+
"""
|
|
36
|
+
算出要擷取的螢幕區域:以游標為中心、邊長為顯示尺寸除以倍率,並限制在螢幕內。
|
|
37
|
+
The screen area to grab: centred on the cursor, sized by the zoom factor,
|
|
38
|
+
and kept inside the screen.
|
|
39
|
+
"""
|
|
40
|
+
side = max(8, int(int(size) / max(MIN_ZOOM, float(zoom))))
|
|
41
|
+
left, top, right, bottom = bounds
|
|
42
|
+
x = int(center[0]) - side // 2
|
|
43
|
+
y = int(center[1]) - side // 2
|
|
44
|
+
x = max(left, min(x, max(left, right - side)))
|
|
45
|
+
y = max(top, min(y, max(top, bottom - side)))
|
|
46
|
+
return QRect(x, y, side, side)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MagnifierWidget(BaseWidget):
|
|
50
|
+
"""
|
|
51
|
+
放大鏡:定期擷取游標周圍畫面並放大顯示。擷取來源可注入,方便測試。
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
REFRESH_MS = 60
|
|
55
|
+
|
|
56
|
+
def __init__(self, zoom: float = DEFAULT_ZOOM, size: int = DEFAULT_SIZE) -> None:
|
|
57
|
+
front_engine_logger.info(f"[MagnifierWidget] Init | zoom={zoom}, size={size}")
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.opacity = 1.0
|
|
60
|
+
self.zoom = clamp_zoom(zoom)
|
|
61
|
+
self.view_size = max(80, int(size))
|
|
62
|
+
self.captured: Optional[QPixmap] = None
|
|
63
|
+
self._cursor_provider = QCursor.pos
|
|
64
|
+
self._grabber = self._grab_screen
|
|
65
|
+
self._timer = QTimer(self)
|
|
66
|
+
self._timer.timeout.connect(self.refresh)
|
|
67
|
+
|
|
68
|
+
def set_cursor_provider(self, provider) -> None:
|
|
69
|
+
self._cursor_provider = provider
|
|
70
|
+
|
|
71
|
+
def set_grabber(self, grabber) -> None:
|
|
72
|
+
"""注入擷取函式:接收 QRect,回傳 QPixmap(測試用)。"""
|
|
73
|
+
self._grabber = grabber
|
|
74
|
+
|
|
75
|
+
def set_zoom(self, zoom: Optional[float] = None, size: Optional[int] = None) -> None:
|
|
76
|
+
if zoom is not None:
|
|
77
|
+
self.zoom = clamp_zoom(zoom)
|
|
78
|
+
if size is not None:
|
|
79
|
+
self.view_size = max(80, int(size))
|
|
80
|
+
self.resize(self.view_size, self.view_size)
|
|
81
|
+
self.update()
|
|
82
|
+
|
|
83
|
+
def start_following(self, interval_ms: int = REFRESH_MS) -> None:
|
|
84
|
+
self.refresh()
|
|
85
|
+
self._timer.start(max(16, int(interval_ms)))
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _grab_screen(rect: QRect) -> Optional[QPixmap]:
|
|
89
|
+
"""用 Qt 擷取指定螢幕區域;沒有可用螢幕時回傳 None。"""
|
|
90
|
+
screen = QGuiApplication.primaryScreen()
|
|
91
|
+
if screen is None:
|
|
92
|
+
return None
|
|
93
|
+
return screen.grabWindow(0, rect.x(), rect.y(), rect.width(), rect.height())
|
|
94
|
+
|
|
95
|
+
def screen_bounds(self) -> Tuple[int, int, int, int]:
|
|
96
|
+
screen = self.screen() or QGuiApplication.primaryScreen()
|
|
97
|
+
if screen is None:
|
|
98
|
+
return (0, 0, 1920, 1080)
|
|
99
|
+
geometry = screen.geometry()
|
|
100
|
+
return (geometry.left(), geometry.top(), geometry.right(), geometry.bottom())
|
|
101
|
+
|
|
102
|
+
def refresh(self) -> None:
|
|
103
|
+
"""擷取游標周圍的畫面 / Grab the area around the cursor."""
|
|
104
|
+
try:
|
|
105
|
+
position = self._cursor_provider()
|
|
106
|
+
rect = source_rect((position.x(), position.y()), self.view_size, self.zoom,
|
|
107
|
+
self.screen_bounds())
|
|
108
|
+
self.captured = self._grabber(rect)
|
|
109
|
+
except Exception as error: # pragma: no cover - capture boundary
|
|
110
|
+
front_engine_logger.debug(f"[MagnifierWidget] capture failed: {error!r}")
|
|
111
|
+
return
|
|
112
|
+
self.update()
|
|
113
|
+
|
|
114
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
115
|
+
if self.captured is None or self.captured.isNull():
|
|
116
|
+
return
|
|
117
|
+
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
|
118
|
+
target = QRect(0, 0, self.width(), self.height())
|
|
119
|
+
painter.drawPixmap(target, self.captured)
|
|
120
|
+
painter.setPen(QColor(255, 255, 255, 180))
|
|
121
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
122
|
+
painter.drawRect(target.adjusted(0, 0, -1, -1))
|
|
123
|
+
|
|
124
|
+
def closeEvent(self, event) -> None:
|
|
125
|
+
if self._timer.isActive():
|
|
126
|
+
self._timer.stop()
|
|
127
|
+
super().closeEvent(event)
|