frontengine 1.0.47__tar.gz → 1.0.48__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.47 → frontengine-1.0.48}/PKG-INFO +16 -1
- {frontengine-1.0.47 → frontengine-1.0.48}/README.md +15 -0
- frontengine-1.0.48/frontengine/show/canvas/whiteboard_widget.py +243 -0
- frontengine-1.0.48/frontengine/ui/dialog/signage_dialog.py +100 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/main_ui.py +22 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/settings_menu.py +18 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/pet/pet_setting_ui.py +25 -4
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/presentation/presentation_setting_ui.py +49 -3
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/user_setting/user_setting_file.py +3 -0
- frontengine-1.0.48/frontengine/utils/audio_meter/microphone_meter.py +147 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/english.py +12 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/traditional_chinese.py +12 -0
- frontengine-1.0.48/frontengine/utils/signage/signage_service.py +156 -0
- frontengine-1.0.48/frontengine/utils/workshop/__init__.py +0 -0
- frontengine-1.0.48/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/PKG-INFO +16 -1
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/SOURCES.txt +6 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/pyproject.toml +1 -1
- {frontengine-1.0.47 → frontengine-1.0.48}/LICENSE +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/__main__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/base_widget.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/camera/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/camera/camera_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/capture → frontengine-1.0.48/frontengine/show/canvas}/__init__.py +0 -0
- {frontengine-1.0.47/frontengine/show/focus_shield → frontengine-1.0.48/frontengine/show/capture}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/capture/region_capture.py +0 -0
- {frontengine-1.0.47/frontengine/show/gaming → frontengine-1.0.48/frontengine/show/focus_shield}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/focus_shield/focus_shield_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/gif → frontengine-1.0.48/frontengine/show/gaming}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/gaming/crosshair_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/image → frontengine-1.0.48/frontengine/show/gif}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.47/frontengine/show/load → frontengine-1.0.48/frontengine/show/image}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.47/frontengine/show/measure → frontengine-1.0.48/frontengine/show/load}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.47/frontengine/show/monitor → frontengine-1.0.48/frontengine/show/measure}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/measure/measure_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/notes → frontengine-1.0.48/frontengine/show/monitor}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/monitor/monitor_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/particle → frontengine-1.0.48/frontengine/show/notes}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/notes/sticky_note_widget.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.47/frontengine/show/pet → frontengine-1.0.48/frontengine/show/particle}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.47/frontengine/show/presentation → frontengine-1.0.48/frontengine/show/pet}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/pet/desktop_pet.py +0 -0
- {frontengine-1.0.47/frontengine/show/scene → frontengine-1.0.48/frontengine/show/presentation}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/annotation_overlay.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/cursor_effects.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/keystroke_display.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/magnifier.py +0 -0
- {frontengine-1.0.47/frontengine/show/screen_care → frontengine-1.0.48/frontengine/show/scene}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.47/frontengine/show/sound_player → frontengine-1.0.48/frontengine/show/screen_care}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/screen_care/color_vision_widget.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/screen_care/screen_filter.py +0 -0
- {frontengine-1.0.47/frontengine/show/spectrum → frontengine-1.0.48/frontengine/show/sound_player}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.47/frontengine/show/teleprompter → frontengine-1.0.48/frontengine/show/spectrum}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/spectrum/spectrum_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/text → frontengine-1.0.48/frontengine/show/teleprompter}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/teleprompter/teleprompter_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/toast → frontengine-1.0.48/frontengine/show/text}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/text/draw_text.py +0 -0
- {frontengine-1.0.47/frontengine/show/video → frontengine-1.0.48/frontengine/show/toast}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/toast/toast_widget.py +0 -0
- {frontengine-1.0.47/frontengine/show/wallpaper → frontengine-1.0.48/frontengine/show/video}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.47/frontengine/show/web → frontengine-1.0.48/frontengine/show/wallpaper}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/wallpaper/wallpaper_widget.py +0 -0
- {frontengine-1.0.47/frontengine/system_tray → frontengine-1.0.48/frontengine/show/web}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.47/frontengine/ui → frontengine-1.0.48/frontengine/system_tray}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.47/frontengine/ui/color → frontengine-1.0.48/frontengine/ui}/__init__.py +0 -0
- {frontengine-1.0.47/frontengine/ui/dialog → frontengine-1.0.48/frontengine/ui/color}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.47/frontengine/ui/menu → frontengine-1.0.48/frontengine/ui/dialog}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/app_profile_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/clipboard_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/reminder_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/remote_control_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/screen_privacy_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/screen_text_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/smart_pause_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/usage_report_dialog.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/window_pin_dialog.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page → frontengine-1.0.48/frontengine/ui/menu}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/control_center → frontengine-1.0.48/frontengine/ui/page}/__init__.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/focus → frontengine-1.0.48/frontengine/ui/page/control_center}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/gif → frontengine-1.0.48/frontengine/ui/page/focus}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/focus/focus_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/image → frontengine-1.0.48/frontengine/ui/page/gif}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/particle → frontengine-1.0.48/frontengine/ui/page/image}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/pet → frontengine-1.0.48/frontengine/ui/page/particle}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/presentation → frontengine-1.0.48/frontengine/ui/page/pet}/__init__.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/scene_setting → frontengine-1.0.48/frontengine/ui/page/presentation}/__init__.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.48/frontengine/ui/page/scene_setting}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/screen_care → frontengine-1.0.48/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/sound_player → frontengine-1.0.48/frontengine/ui/page/screen_care}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/screen_care/screen_care_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/stream → frontengine-1.0.48/frontengine/ui/page/sound_player}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/text → frontengine-1.0.48/frontengine/ui/page/stream}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/stream/stream_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/tools → frontengine-1.0.48/frontengine/ui/page/text}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/video → frontengine-1.0.48/frontengine/ui/page/tools}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/tools/tools_setting_ui.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/wallpaper → frontengine-1.0.48/frontengine/ui/page/video}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/web → frontengine-1.0.48/frontengine/ui/page/wallpaper}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/wallpaper/wallpaper_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/ui/page/widgets → frontengine-1.0.48/frontengine/ui/page/web}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/user_setting → frontengine-1.0.48/frontengine/ui/page/widgets}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/widgets/widgets_setting_ui.py +0 -0
- {frontengine-1.0.47/frontengine/utils → frontengine-1.0.48/frontengine/user_setting}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.47/frontengine/utils/app_profile → frontengine-1.0.48/frontengine/utils}/__init__.py +0 -0
- {frontengine-1.0.47/frontengine/utils/audio_meter → frontengine-1.0.48/frontengine/utils/app_profile}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/app_profile/app_profile_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/autostart → frontengine-1.0.48/frontengine/utils/audio_meter}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/loopback_capture.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/spectrum_analyzer.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.47/frontengine/utils/break_reminder → frontengine-1.0.48/frontengine/utils/autostart}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/autostart/autostart_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/browser → frontengine-1.0.48/frontengine/utils/break_reminder}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/break_reminder/break_reminder.py +0 -0
- {frontengine-1.0.47/frontengine/utils/clipboard → frontengine-1.0.48/frontengine/utils/browser}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.47/frontengine/utils/color_vision → frontengine-1.0.48/frontengine/utils/clipboard}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/clipboard/clipboard_history.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/clipboard/clipboard_watcher.py +0 -0
- {frontengine-1.0.47/frontengine/utils/critical_exit → frontengine-1.0.48/frontengine/utils/color_vision}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/color_vision/color_vision.py +0 -0
- {frontengine-1.0.47/frontengine/utils/file → frontengine-1.0.48/frontengine/utils/critical_exit}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.47/frontengine/utils/focus_shield → frontengine-1.0.48/frontengine/utils/file}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.47/frontengine/utils/focus_timer → frontengine-1.0.48/frontengine/utils/focus_shield}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/focus_shield/focus_shield.py +0 -0
- {frontengine-1.0.47/frontengine/utils/hotkey → frontengine-1.0.48/frontengine/utils/focus_timer}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/focus_timer/focus_timer.py +0 -0
- {frontengine-1.0.47/frontengine/utils/input_watch → frontengine-1.0.48/frontengine/utils/hotkey}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/json → frontengine-1.0.48/frontengine/utils/input_watch}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/input_watch/input_watch_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/logging → frontengine-1.0.48/frontengine/utils/json}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.47/frontengine/utils/measure → frontengine-1.0.48/frontengine/utils/logging}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.47/frontengine/utils/midi → frontengine-1.0.48/frontengine/utils/measure}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/measure/measure.py +0 -0
- {frontengine-1.0.47/frontengine/utils/multi_language → frontengine-1.0.48/frontengine/utils/midi}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/midi/midi_input.py +0 -0
- {frontengine-1.0.47/frontengine/utils/now_playing → frontengine-1.0.48/frontengine/utils/multi_language}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.47/frontengine/utils/obs → frontengine-1.0.48/frontengine/utils/now_playing}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/now_playing/now_playing.py +0 -0
- {frontengine-1.0.47/frontengine/utils/pet_chat → frontengine-1.0.48/frontengine/utils/obs}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/obs/obs_client.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/obs/obs_protocol.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/obs/websocket_frames.py +0 -0
- {frontengine-1.0.47/frontengine/utils/platform_info → frontengine-1.0.48/frontengine/utils/pet_chat}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/playlist → frontengine-1.0.48/frontengine/utils/platform_info}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/platform_info/platform_info.py +0 -0
- {frontengine-1.0.47/frontengine/utils/plugins → frontengine-1.0.48/frontengine/utils/playlist}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/playlist/playlist.py +0 -0
- {frontengine-1.0.47/frontengine/utils/power_mode → frontengine-1.0.48/frontengine/utils/plugins}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/plugins/plugin_loader.py +0 -0
- {frontengine-1.0.47/frontengine/utils/preset_schedule → frontengine-1.0.48/frontengine/utils/power_mode}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/power_mode/power_mode.py +0 -0
- {frontengine-1.0.47/frontengine/utils/recording → frontengine-1.0.48/frontengine/utils/preset_schedule}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/redirect_manager → frontengine-1.0.48/frontengine/utils/recording}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/recording/frame_recorder.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/recording/gif_writer.py +0 -0
- {frontengine-1.0.47/frontengine/utils/reminder → frontengine-1.0.48/frontengine/utils/redirect_manager}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.47/frontengine/utils/remote → frontengine-1.0.48/frontengine/utils/reminder}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/reminder/reminder_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/screen_privacy → frontengine-1.0.48/frontengine/utils/remote}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/remote/remote_server.py +0 -0
- {frontengine-1.0.47/frontengine/utils/screen_text → frontengine-1.0.48/frontengine/utils/screen_privacy}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/screen_privacy/capture_affinity.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/screen_privacy/share_watch.py +0 -0
- {frontengine-1.0.47/frontengine/utils/smart_pause → frontengine-1.0.48/frontengine/utils/screen_text}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/screen_text/screen_text_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/soundboard → frontengine-1.0.48/frontengine/utils/signage}/__init__.py +0 -0
- {frontengine-1.0.47/frontengine/utils/system_stats → frontengine-1.0.48/frontengine/utils/smart_pause}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/smart_pause/pause_rules.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/text_source → frontengine-1.0.48/frontengine/utils/soundboard}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/soundboard/soundboard.py +0 -0
- {frontengine-1.0.47/frontengine/utils/theme_schedule → frontengine-1.0.48/frontengine/utils/system_stats}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/system_stats/system_stats.py +0 -0
- {frontengine-1.0.47/frontengine/utils/usage_tracking → frontengine-1.0.48/frontengine/utils/text_source}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/text_source/text_source.py +0 -0
- {frontengine-1.0.47/frontengine/utils/virtual_camera → frontengine-1.0.48/frontengine/utils/theme_schedule}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.47/frontengine/utils/weather → frontengine-1.0.48/frontengine/utils/usage_tracking}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/usage_tracking/usage_service.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/usage_tracking/usage_tracker.py +0 -0
- {frontengine-1.0.47/frontengine/utils/window_pin → frontengine-1.0.48/frontengine/utils/virtual_camera}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/virtual_camera/camera_feed.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/virtual_camera/virtual_camera.py +0 -0
- {frontengine-1.0.47/frontengine/utils/workshop → frontengine-1.0.48/frontengine/utils/weather}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/weather/weather_service.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.47/frontengine/worker → frontengine-1.0.48/frontengine/utils/window_pin}/__init__.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/window_pin/window_layout.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/window_pin/window_pin.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/workshop/workshop_content.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.47 → frontengine-1.0.48}/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.48
|
|
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
|
|
@@ -204,6 +204,21 @@ Four things on the **Stream** tab, for playing and for going live:
|
|
|
204
204
|
|
|
205
205
|
---
|
|
206
206
|
|
|
207
|
+
## Signage, whiteboard and lip-sync
|
|
208
|
+
|
|
209
|
+
- **Signage mode** (Settings) — rotate through a list of presets on a timer and
|
|
210
|
+
put the main window away, for a machine left running as a display. The
|
|
211
|
+
rotation wraps, so it keeps cycling.
|
|
212
|
+
- **Whiteboard** (Presenting tab) — an infinite canvas: drag with the middle
|
|
213
|
+
button to pan, scroll to zoom, and save what you drew as an image. Strokes
|
|
214
|
+
live in canvas coordinates, so panning and zooming leaves them where they
|
|
215
|
+
belong.
|
|
216
|
+
- **Pet lip-sync** — the pet can react to your **microphone** instead of your
|
|
217
|
+
speakers, so it moves while you talk. Like the speaker reaction this reads
|
|
218
|
+
only the meter — a number, not sound. Windows only.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
207
222
|
## Remote control
|
|
208
223
|
|
|
209
224
|
From **Settings → Remote control**:
|
|
@@ -180,6 +180,21 @@ Four things on the **Stream** tab, for playing and for going live:
|
|
|
180
180
|
|
|
181
181
|
---
|
|
182
182
|
|
|
183
|
+
## Signage, whiteboard and lip-sync
|
|
184
|
+
|
|
185
|
+
- **Signage mode** (Settings) — rotate through a list of presets on a timer and
|
|
186
|
+
put the main window away, for a machine left running as a display. The
|
|
187
|
+
rotation wraps, so it keeps cycling.
|
|
188
|
+
- **Whiteboard** (Presenting tab) — an infinite canvas: drag with the middle
|
|
189
|
+
button to pan, scroll to zoom, and save what you drew as an image. Strokes
|
|
190
|
+
live in canvas coordinates, so panning and zooming leaves them where they
|
|
191
|
+
belong.
|
|
192
|
+
- **Pet lip-sync** — the pet can react to your **microphone** instead of your
|
|
193
|
+
speakers, so it moves while you talk. Like the speaker reaction this reads
|
|
194
|
+
only the meter — a number, not sound. Windows only.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
183
198
|
## Remote control
|
|
184
199
|
|
|
185
200
|
From **Settings → Remote control**:
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
無限畫布白板:可以一直往外畫、拖曳平移、滾輪縮放,最後存成圖片。
|
|
3
|
+
|
|
4
|
+
和簡報分頁的塗鴉層不同的是,筆畫存的是「畫布座標」而不是螢幕座標,所以平移
|
|
5
|
+
縮放之後線條還在它原本該在的地方——這正是「無限」的意思。
|
|
6
|
+
|
|
7
|
+
An infinite whiteboard: keep drawing outwards, drag to pan, scroll to zoom, and
|
|
8
|
+
save the result as an image.
|
|
9
|
+
|
|
10
|
+
Unlike the presenting page's annotation layer, strokes are stored in canvas
|
|
11
|
+
coordinates rather than screen ones, so panning and zooming leaves them where
|
|
12
|
+
they belong - which is what makes it infinite.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from PySide6.QtCore import QPoint, QPointF, QRectF, Qt
|
|
20
|
+
from PySide6.QtGui import QColor, QImage, QPainter, QPen
|
|
21
|
+
|
|
22
|
+
from frontengine.show.base_widget import BaseWidget
|
|
23
|
+
from frontengine.show.window_helpers import apply_overlay_window_flags
|
|
24
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
25
|
+
|
|
26
|
+
DEFAULT_COLOR = "#ffd54f"
|
|
27
|
+
DEFAULT_WIDTH = 4
|
|
28
|
+
MIN_ZOOM = 0.2
|
|
29
|
+
MAX_ZOOM = 5.0
|
|
30
|
+
ZOOM_STEP = 1.15
|
|
31
|
+
_SAVE_MARGIN = 24
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def clamp_zoom(value: Any, fallback: float = 1.0) -> float:
|
|
35
|
+
"""縮放倍率夾在看得清楚又不會迷路的範圍。"""
|
|
36
|
+
try:
|
|
37
|
+
return max(MIN_ZOOM, min(MAX_ZOOM, float(value)))
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return fallback
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def to_canvas(point: Tuple[float, float], offset: Tuple[float, float], zoom: float) -> Tuple[float, float]:
|
|
43
|
+
"""螢幕座標 -> 畫布座標。"""
|
|
44
|
+
scale = clamp_zoom(zoom)
|
|
45
|
+
return ((point[0] - offset[0]) / scale, (point[1] - offset[1]) / scale)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_screen(point: Tuple[float, float], offset: Tuple[float, float], zoom: float) -> Tuple[float, float]:
|
|
49
|
+
"""畫布座標 -> 螢幕座標。"""
|
|
50
|
+
scale = clamp_zoom(zoom)
|
|
51
|
+
return (point[0] * scale + offset[0], point[1] * scale + offset[1])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def content_bounds(strokes: List[Dict[str, Any]]) -> Optional[Tuple[float, float, float, float]]:
|
|
55
|
+
"""
|
|
56
|
+
所有筆畫涵蓋的畫布範圍 (left, top, right, bottom);沒有筆畫回傳 None。
|
|
57
|
+
存檔時用它決定要輸出多大,才不會存出一大片空白。
|
|
58
|
+
The canvas area the strokes cover, or None when there are none. Saving uses
|
|
59
|
+
it to size the image, so the result is not mostly blank.
|
|
60
|
+
"""
|
|
61
|
+
points = [point for stroke in strokes for point in stroke.get("points", [])]
|
|
62
|
+
if not points:
|
|
63
|
+
return None
|
|
64
|
+
xs = [point[0] for point in points]
|
|
65
|
+
ys = [point[1] for point in points]
|
|
66
|
+
return (min(xs), min(ys), max(xs), max(ys))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class WhiteboardWidget(BaseWidget):
|
|
70
|
+
"""
|
|
71
|
+
無限畫布。筆畫以畫布座標保存,畫的時候才換算成螢幕座標。
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, color: str = DEFAULT_COLOR, width: int = DEFAULT_WIDTH) -> None:
|
|
75
|
+
front_engine_logger.info("[WhiteboardWidget] Init")
|
|
76
|
+
super().__init__()
|
|
77
|
+
candidate = QColor(color)
|
|
78
|
+
self.color = candidate if candidate.isValid() else QColor(DEFAULT_COLOR)
|
|
79
|
+
self.pen_width = max(1, int(width))
|
|
80
|
+
self.opacity = 1.0
|
|
81
|
+
self.overlay_lockable = False
|
|
82
|
+
self.strokes: List[Dict[str, Any]] = []
|
|
83
|
+
self.zoom = 1.0
|
|
84
|
+
self.offset = [0.0, 0.0]
|
|
85
|
+
self._stroke: Optional[Dict[str, Any]] = None
|
|
86
|
+
self._pan_origin: Optional[QPoint] = None
|
|
87
|
+
apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
|
|
88
|
+
|
|
89
|
+
# --- drawing ---------------------------------------------------------
|
|
90
|
+
def begin_stroke(self, point: QPoint) -> None:
|
|
91
|
+
"""開始一筆。"""
|
|
92
|
+
self._stroke = {"color": self.color.name(), "width": self.pen_width,
|
|
93
|
+
"points": [to_canvas((point.x(), point.y()), self.offset, self.zoom)]}
|
|
94
|
+
|
|
95
|
+
def extend_stroke(self, point: QPoint) -> None:
|
|
96
|
+
if self._stroke is not None:
|
|
97
|
+
self._stroke["points"].append(to_canvas((point.x(), point.y()), self.offset, self.zoom))
|
|
98
|
+
self.update()
|
|
99
|
+
|
|
100
|
+
def end_stroke(self) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
收筆。只有一個點的「筆畫」是誤點,丟掉不留。
|
|
103
|
+
Finish. A one-point stroke is a stray click and is discarded.
|
|
104
|
+
"""
|
|
105
|
+
stroke, self._stroke = self._stroke, None
|
|
106
|
+
if stroke is None or len(stroke["points"]) < 2:
|
|
107
|
+
return False
|
|
108
|
+
self.strokes.append(stroke)
|
|
109
|
+
self.update()
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
def undo(self) -> bool:
|
|
113
|
+
"""收回最後一筆。"""
|
|
114
|
+
if not self.strokes:
|
|
115
|
+
return False
|
|
116
|
+
self.strokes.pop()
|
|
117
|
+
self.update()
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
def clear(self) -> None:
|
|
121
|
+
self.strokes = []
|
|
122
|
+
self._stroke = None
|
|
123
|
+
self.update()
|
|
124
|
+
|
|
125
|
+
def set_pen(self, color: Optional[str] = None, width: Optional[int] = None) -> None:
|
|
126
|
+
if color is not None:
|
|
127
|
+
candidate = QColor(color)
|
|
128
|
+
if candidate.isValid():
|
|
129
|
+
self.color = candidate
|
|
130
|
+
if width is not None:
|
|
131
|
+
self.pen_width = max(1, int(width))
|
|
132
|
+
|
|
133
|
+
# --- navigating ------------------------------------------------------
|
|
134
|
+
def pan(self, dx: float, dy: float) -> None:
|
|
135
|
+
"""平移畫布。"""
|
|
136
|
+
self.offset[0] += float(dx)
|
|
137
|
+
self.offset[1] += float(dy)
|
|
138
|
+
self.update()
|
|
139
|
+
|
|
140
|
+
def zoom_at(self, point: QPoint, factor: float) -> float:
|
|
141
|
+
"""
|
|
142
|
+
以游標為中心縮放:游標底下的那一點在縮放前後要停在原地,
|
|
143
|
+
不然放大時畫面會整個跑掉。
|
|
144
|
+
Zoom around the cursor so whatever is under it stays put; otherwise the
|
|
145
|
+
view lurches away every time you zoom.
|
|
146
|
+
"""
|
|
147
|
+
before = to_canvas((point.x(), point.y()), self.offset, self.zoom)
|
|
148
|
+
self.zoom = clamp_zoom(self.zoom * float(factor))
|
|
149
|
+
after = to_screen(before, self.offset, self.zoom)
|
|
150
|
+
self.offset[0] += point.x() - after[0]
|
|
151
|
+
self.offset[1] += point.y() - after[1]
|
|
152
|
+
self.update()
|
|
153
|
+
return self.zoom
|
|
154
|
+
|
|
155
|
+
def reset_view(self) -> None:
|
|
156
|
+
"""回到原點與原始比例(筆畫都還在)。"""
|
|
157
|
+
self.zoom = 1.0
|
|
158
|
+
self.offset = [0.0, 0.0]
|
|
159
|
+
self.update()
|
|
160
|
+
|
|
161
|
+
# --- events ----------------------------------------------------------
|
|
162
|
+
def mousePressEvent(self, event) -> None:
|
|
163
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
164
|
+
self.begin_stroke(event.position().toPoint())
|
|
165
|
+
elif event.button() == Qt.MouseButton.MiddleButton:
|
|
166
|
+
self._pan_origin = event.position().toPoint()
|
|
167
|
+
super().mousePressEvent(event)
|
|
168
|
+
|
|
169
|
+
def mouseMoveEvent(self, event) -> None:
|
|
170
|
+
point = event.position().toPoint()
|
|
171
|
+
if self._pan_origin is not None:
|
|
172
|
+
self.pan(point.x() - self._pan_origin.x(), point.y() - self._pan_origin.y())
|
|
173
|
+
self._pan_origin = point
|
|
174
|
+
else:
|
|
175
|
+
self.extend_stroke(point)
|
|
176
|
+
super().mouseMoveEvent(event)
|
|
177
|
+
|
|
178
|
+
def mouseReleaseEvent(self, event) -> None:
|
|
179
|
+
if event.button() == Qt.MouseButton.MiddleButton:
|
|
180
|
+
self._pan_origin = None
|
|
181
|
+
else:
|
|
182
|
+
self.end_stroke()
|
|
183
|
+
super().mouseReleaseEvent(event)
|
|
184
|
+
|
|
185
|
+
def wheelEvent(self, event) -> None:
|
|
186
|
+
step = ZOOM_STEP if event.angleDelta().y() > 0 else 1.0 / ZOOM_STEP
|
|
187
|
+
self.zoom_at(event.position().toPoint(), step)
|
|
188
|
+
|
|
189
|
+
# --- output ----------------------------------------------------------
|
|
190
|
+
def save_image(self, path: str) -> Optional[str]:
|
|
191
|
+
"""
|
|
192
|
+
把所有筆畫存成一張圖(只涵蓋畫過的範圍)。沒有筆畫就不存,回傳 None。
|
|
193
|
+
Save the strokes as an image covering only the area drawn on. Nothing
|
|
194
|
+
drawn means nothing saved.
|
|
195
|
+
"""
|
|
196
|
+
bounds = content_bounds(self.strokes)
|
|
197
|
+
if bounds is None:
|
|
198
|
+
return None
|
|
199
|
+
left, top, right, bottom = bounds
|
|
200
|
+
width = int(right - left) + _SAVE_MARGIN * 2
|
|
201
|
+
height = int(bottom - top) + _SAVE_MARGIN * 2
|
|
202
|
+
image = QImage(max(1, width), max(1, height), QImage.Format.Format_ARGB32)
|
|
203
|
+
image.fill(Qt.GlobalColor.transparent)
|
|
204
|
+
painter = QPainter(image)
|
|
205
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
206
|
+
for stroke in self.strokes:
|
|
207
|
+
self._paint_stroke(painter, stroke,
|
|
208
|
+
offset=(_SAVE_MARGIN - left, _SAVE_MARGIN - top), zoom=1.0)
|
|
209
|
+
painter.end()
|
|
210
|
+
target = Path(path)
|
|
211
|
+
try:
|
|
212
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
if not image.save(str(target)):
|
|
214
|
+
return None
|
|
215
|
+
except OSError as error:
|
|
216
|
+
front_engine_logger.warning(f"[WhiteboardWidget] save failed: {error!r}")
|
|
217
|
+
return None
|
|
218
|
+
front_engine_logger.info(f"[WhiteboardWidget] saved | {target}")
|
|
219
|
+
return str(target)
|
|
220
|
+
|
|
221
|
+
def _paint_stroke(self, painter: QPainter, stroke: Dict[str, Any],
|
|
222
|
+
offset, zoom: float) -> None:
|
|
223
|
+
points = stroke.get("points", [])
|
|
224
|
+
if len(points) < 2:
|
|
225
|
+
return
|
|
226
|
+
pen = QPen(QColor(stroke.get("color", DEFAULT_COLOR)),
|
|
227
|
+
max(1.0, float(stroke.get("width", DEFAULT_WIDTH)) * zoom))
|
|
228
|
+
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
229
|
+
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
230
|
+
painter.setPen(pen)
|
|
231
|
+
previous = to_screen(points[0], offset, zoom)
|
|
232
|
+
for point in points[1:]:
|
|
233
|
+
current = to_screen(point, offset, zoom)
|
|
234
|
+
painter.drawLine(QPointF(*previous), QPointF(*current))
|
|
235
|
+
previous = current
|
|
236
|
+
|
|
237
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
238
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
239
|
+
painter.fillRect(QRectF(self.rect()), QColor(0, 0, 0, 120))
|
|
240
|
+
for stroke in self.strokes:
|
|
241
|
+
self._paint_stroke(painter, stroke, self.offset, self.zoom)
|
|
242
|
+
if self._stroke is not None:
|
|
243
|
+
self._paint_stroke(painter, self._stroke, self.offset, self.zoom)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
看板模式設定:依序輪播一組預設集,適合把機器擺著當展示螢幕。
|
|
3
|
+
|
|
4
|
+
Signage settings: rotate through a set of presets, for a machine left running
|
|
5
|
+
as a display.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from PySide6.QtWidgets import (
|
|
12
|
+
QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QLabel, QPlainTextEdit, QSpinBox, QWidget,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from frontengine.user_setting.user_setting_file import user_setting_dict, write_user_setting
|
|
16
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
17
|
+
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
18
|
+
from frontengine.utils.signage.signage_service import (
|
|
19
|
+
DEFAULT_INTERVAL_MINUTES, MAX_INTERVAL_MINUTES, MIN_INTERVAL_MINUTES, clamp_interval,
|
|
20
|
+
normalize_playlist,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
SETTING_KEY = "signage"
|
|
24
|
+
DEFAULT_SETTINGS: Dict[str, Any] = {
|
|
25
|
+
"enabled": False, "presets": [], "interval_minutes": DEFAULT_INTERVAL_MINUTES,
|
|
26
|
+
"hide_window": True,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _t(key: str, fallback: str) -> str:
|
|
31
|
+
return language_wrapper.language_word_dict.get(key, fallback)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def current_settings() -> Dict[str, Any]:
|
|
35
|
+
"""目前的看板設定(缺項目補預設值)。"""
|
|
36
|
+
stored = user_setting_dict.get(SETTING_KEY)
|
|
37
|
+
settings = dict(DEFAULT_SETTINGS)
|
|
38
|
+
if isinstance(stored, dict):
|
|
39
|
+
settings.update({key: stored[key] for key in DEFAULT_SETTINGS if key in stored})
|
|
40
|
+
settings["presets"] = normalize_playlist(settings.get("presets"))
|
|
41
|
+
settings["interval_minutes"] = clamp_interval(settings.get("interval_minutes"))
|
|
42
|
+
return settings
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SignageDialog(QDialog):
|
|
46
|
+
"""設定要輪播哪些預設集、多久換一次。"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
|
49
|
+
front_engine_logger.info("[SignageDialog] Init")
|
|
50
|
+
super().__init__(parent)
|
|
51
|
+
self.setWindowTitle(_t("signage_title", "Signage mode"))
|
|
52
|
+
settings = current_settings()
|
|
53
|
+
|
|
54
|
+
self.enable_checkbox = QCheckBox(_t("signage_enable", "Rotate presets"))
|
|
55
|
+
self.enable_checkbox.setChecked(bool(settings.get("enabled")))
|
|
56
|
+
self.presets_label = QLabel(_t("signage_presets_label", "Presets, one per line:"))
|
|
57
|
+
self.presets_edit = QPlainTextEdit("\n".join(settings.get("presets", [])))
|
|
58
|
+
self.interval_label = QLabel(_t("signage_interval_label", "Change every (minutes)"))
|
|
59
|
+
self.interval_spinbox = QSpinBox()
|
|
60
|
+
self.interval_spinbox.setRange(MIN_INTERVAL_MINUTES, MAX_INTERVAL_MINUTES)
|
|
61
|
+
self.interval_spinbox.setValue(settings.get("interval_minutes"))
|
|
62
|
+
self.hide_checkbox = QCheckBox(
|
|
63
|
+
_t("signage_hide_window", "Hide the main window while rotating"))
|
|
64
|
+
self.hide_checkbox.setChecked(bool(settings.get("hide_window", True)))
|
|
65
|
+
self.hint_label = QLabel(
|
|
66
|
+
_t("signage_hint",
|
|
67
|
+
"Presets are saved from the Presets menu. The rotation wraps at the end, so a "
|
|
68
|
+
"machine left running keeps cycling."))
|
|
69
|
+
self.hint_label.setWordWrap(True)
|
|
70
|
+
|
|
71
|
+
self.button_box = QDialogButtonBox(
|
|
72
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
73
|
+
self.button_box.accepted.connect(self.accept)
|
|
74
|
+
self.button_box.rejected.connect(self.reject)
|
|
75
|
+
|
|
76
|
+
layout = QGridLayout(self)
|
|
77
|
+
layout.addWidget(self.enable_checkbox, 0, 0, 1, 2)
|
|
78
|
+
layout.addWidget(self.presets_label, 1, 0, 1, 2)
|
|
79
|
+
layout.addWidget(self.presets_edit, 2, 0, 1, 2)
|
|
80
|
+
layout.addWidget(self.interval_label, 3, 0)
|
|
81
|
+
layout.addWidget(self.interval_spinbox, 3, 1)
|
|
82
|
+
layout.addWidget(self.hide_checkbox, 4, 0, 1, 2)
|
|
83
|
+
layout.addWidget(self.hint_label, 5, 0, 1, 2)
|
|
84
|
+
layout.addWidget(self.button_box, 6, 0, 1, 2)
|
|
85
|
+
|
|
86
|
+
def settings(self) -> Dict[str, Any]:
|
|
87
|
+
"""對話框上目前的設定。"""
|
|
88
|
+
return {
|
|
89
|
+
"enabled": self.enable_checkbox.isChecked(),
|
|
90
|
+
"presets": normalize_playlist(self.presets_edit.toPlainText()),
|
|
91
|
+
"interval_minutes": self.interval_spinbox.value(),
|
|
92
|
+
"hide_window": self.hide_checkbox.isChecked(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def accept(self) -> None:
|
|
96
|
+
settings = self.settings()
|
|
97
|
+
user_setting_dict[SETTING_KEY] = settings
|
|
98
|
+
write_user_setting()
|
|
99
|
+
front_engine_logger.info(f"[SignageDialog] saved | {len(settings['presets'])} preset(s)")
|
|
100
|
+
super().accept()
|
|
@@ -53,6 +53,8 @@ from frontengine.ui.dialog.remote_control_dialog import (
|
|
|
53
53
|
MIDI_KEY, current_bindings, remote_enabled,
|
|
54
54
|
)
|
|
55
55
|
from frontengine.ui.dialog.screen_privacy_dialog import current_settings as current_privacy_settings
|
|
56
|
+
from frontengine.ui.dialog.signage_dialog import current_settings as current_signage_settings
|
|
57
|
+
from frontengine.utils.signage.signage_service import SignageService
|
|
56
58
|
from frontengine.utils.midi.midi_input import MidiInput, binding_key
|
|
57
59
|
from frontengine.utils.remote.remote_server import RemoteServer
|
|
58
60
|
from frontengine.ui.dialog.smart_pause_dialog import current_rules
|
|
@@ -235,6 +237,13 @@ class FrontEngineMainUI(QMainWindow):
|
|
|
235
237
|
if user_setting_dict.get(MIDI_KEY):
|
|
236
238
|
self.midi_input.start(user_setting_dict.get("midi_device") or 0)
|
|
237
239
|
|
|
240
|
+
# 看板模式:依序輪播預設集,主視窗可以收起來
|
|
241
|
+
# Signage mode: rotate presets, with the main window out of the way.
|
|
242
|
+
self.signage_service = SignageService(config_provider=current_signage_settings)
|
|
243
|
+
self.signage_service.preset_due.connect(lambda name: apply_named_preset(self, name))
|
|
244
|
+
if current_signage_settings().get("enabled"):
|
|
245
|
+
self.start_signage()
|
|
246
|
+
|
|
238
247
|
# 分享畫面時把覆蓋層藏出擷取結果(自己仍看得到)
|
|
239
248
|
# Hide the overlays from the capture while sharing; they stay on screen here.
|
|
240
249
|
self.share_watch_service = ShareWatchService(config_provider=current_privacy_settings)
|
|
@@ -508,6 +517,18 @@ class FrontEngineMainUI(QMainWindow):
|
|
|
508
517
|
if action:
|
|
509
518
|
self._handle_hotkey(action)
|
|
510
519
|
|
|
520
|
+
def start_signage(self) -> Optional[str]:
|
|
521
|
+
"""開始輪播;設定要求的話也把主視窗收起來。"""
|
|
522
|
+
first = self.signage_service.start()
|
|
523
|
+
if first is not None and current_signage_settings().get("hide_window", True):
|
|
524
|
+
self.hide()
|
|
525
|
+
return first
|
|
526
|
+
|
|
527
|
+
def stop_signage(self) -> None:
|
|
528
|
+
"""停止輪播並把主視窗放回來。"""
|
|
529
|
+
self.signage_service.stop()
|
|
530
|
+
self.show()
|
|
531
|
+
|
|
511
532
|
def _on_sharing_changed(self, sharing: bool, match: str) -> None:
|
|
512
533
|
"""
|
|
513
534
|
偵測到會議程式開著/關掉時,自動把覆蓋層藏出擷取結果或放回去。
|
|
@@ -573,6 +594,7 @@ class FrontEngineMainUI(QMainWindow):
|
|
|
573
594
|
# The background services to stop on close, outermost first.
|
|
574
595
|
_CLOSING_SERVICES = (
|
|
575
596
|
"preset_schedule_service", "theme_schedule_service", "usage_service",
|
|
597
|
+
"signage_service",
|
|
576
598
|
"remote_server", "midi_input", "share_watch_service", "reminder_service",
|
|
577
599
|
"app_profile_service", "smart_pause_service", "hotkey_service",
|
|
578
600
|
)
|
|
@@ -12,6 +12,7 @@ from frontengine.ui.dialog.hotkey_settings_dialog import HotkeySettingsDialog
|
|
|
12
12
|
from frontengine.ui.dialog.reminder_dialog import ReminderDialog
|
|
13
13
|
from frontengine.ui.dialog.remote_control_dialog import RemoteControlDialog
|
|
14
14
|
from frontengine.ui.dialog.screen_privacy_dialog import ScreenPrivacyDialog
|
|
15
|
+
from frontengine.ui.dialog.signage_dialog import SignageDialog
|
|
15
16
|
from frontengine.ui.dialog.smart_pause_dialog import SmartPauseDialog
|
|
16
17
|
from frontengine.ui.dialog.usage_report_dialog import UsageReportDialog
|
|
17
18
|
from frontengine.user_setting.user_setting_file import (
|
|
@@ -89,6 +90,11 @@ def build_settings_menu(ui: "FrontEngineMainUI") -> None:
|
|
|
89
90
|
menu.addAction(reminder_action)
|
|
90
91
|
ui.reminder_action = reminder_action
|
|
91
92
|
|
|
93
|
+
signage_action = QAction(_t("settings_menu_signage", "Signage mode..."), menu)
|
|
94
|
+
signage_action.triggered.connect(lambda: _open_signage_dialog(ui))
|
|
95
|
+
menu.addAction(signage_action)
|
|
96
|
+
ui.signage_action = signage_action
|
|
97
|
+
|
|
92
98
|
remote_action = QAction(_t("settings_menu_remote", "Remote control..."), menu)
|
|
93
99
|
remote_action.triggered.connect(lambda: _open_remote_dialog(ui))
|
|
94
100
|
menu.addAction(remote_action)
|
|
@@ -127,6 +133,18 @@ def build_settings_menu(ui: "FrontEngineMainUI") -> None:
|
|
|
127
133
|
menu.addAction(import_action)
|
|
128
134
|
|
|
129
135
|
|
|
136
|
+
def _open_signage_dialog(ui: "FrontEngineMainUI") -> None:
|
|
137
|
+
"""開看板設定;按下確定後立刻套用新的輪播狀態。"""
|
|
138
|
+
front_engine_logger.info("[SettingsMenu] open SignageDialog")
|
|
139
|
+
dialog = SignageDialog(ui)
|
|
140
|
+
if dialog.exec() != QDialog.DialogCode.Accepted:
|
|
141
|
+
return
|
|
142
|
+
if dialog.settings().get("enabled"):
|
|
143
|
+
ui.start_signage()
|
|
144
|
+
else:
|
|
145
|
+
ui.stop_signage()
|
|
146
|
+
|
|
147
|
+
|
|
130
148
|
def _open_remote_dialog(ui: "FrontEngineMainUI") -> None:
|
|
131
149
|
"""開遙控設定,並把它接上 MIDI 的「學習」流程。"""
|
|
132
150
|
front_engine_logger.info("[SettingsMenu] open RemoteControlDialog")
|
|
@@ -21,6 +21,7 @@ from frontengine.ui.page.utils import (
|
|
|
21
21
|
resolve_preferred_monitor,
|
|
22
22
|
)
|
|
23
23
|
from frontengine.user_setting.user_setting_file import add_recent_file
|
|
24
|
+
from frontengine.utils.audio_meter.microphone_meter import microphone_level
|
|
24
25
|
from frontengine.utils.audio_meter.screen_audio import audio_level_provider_for_screen
|
|
25
26
|
from frontengine.utils.pet_chat.pet_chat_service import PetChatService
|
|
26
27
|
from frontengine.utils.focus_timer.focus_timer import (
|
|
@@ -142,6 +143,12 @@ class PetSettingUI(QWidget):
|
|
|
142
143
|
for label, value in (("0%", 0), ("25%", 25), ("50%", 50), ("75%", 75), ("100%", 100)):
|
|
143
144
|
self.volume_combobox.addItem(label, value)
|
|
144
145
|
self.volume_combobox.setCurrentText("100%")
|
|
146
|
+
self.audio_source_combobox = QComboBox()
|
|
147
|
+
self.audio_source_combobox.addItem(
|
|
148
|
+
language_wrapper.language_word_dict.get("pet_audio_speakers", "Speakers"), "speakers")
|
|
149
|
+
self.audio_source_combobox.addItem(
|
|
150
|
+
language_wrapper.language_word_dict.get("pet_audio_microphone", "Microphone"),
|
|
151
|
+
"microphone")
|
|
145
152
|
self.audio_react_checkbox = QCheckBox(
|
|
146
153
|
language_wrapper.language_word_dict.get("pet_audio_label", "React to audio"))
|
|
147
154
|
self.drop_hint_label = QLabel(
|
|
@@ -185,6 +192,7 @@ class PetSettingUI(QWidget):
|
|
|
185
192
|
self.grid_layout.addWidget(self.volume_label, 6, 2)
|
|
186
193
|
self.grid_layout.addWidget(self.volume_combobox, 7, 2)
|
|
187
194
|
self.grid_layout.addWidget(self.audio_react_checkbox, 7, 0)
|
|
195
|
+
self.grid_layout.addWidget(self.audio_source_combobox, 7, 1)
|
|
188
196
|
self.grid_layout.addWidget(self.tag_checkbox, 7, 1)
|
|
189
197
|
self.grid_layout.addWidget(self.speech_checkbox, 7, 2)
|
|
190
198
|
self.grid_layout.addWidget(self.focus_label, 8, 0)
|
|
@@ -218,10 +226,7 @@ class PetSettingUI(QWidget):
|
|
|
218
226
|
pet.set_chat_service(self.chat_service())
|
|
219
227
|
geometry = self._target_geometry()
|
|
220
228
|
if self.audio_react_checkbox.isChecked():
|
|
221
|
-
|
|
222
|
-
# Follow the audio of the screen the pet lives on; default endpoint otherwise.
|
|
223
|
-
pet.set_audio_level_provider(
|
|
224
|
-
audio_level_provider_for_screen(geometry[1] if geometry is not None else None))
|
|
229
|
+
pet.set_audio_level_provider(self._audio_provider(geometry))
|
|
225
230
|
pet.set_pet_window_flag()
|
|
226
231
|
self.pet_list.append(pet)
|
|
227
232
|
pet.show()
|
|
@@ -420,6 +425,20 @@ class PetSettingUI(QWidget):
|
|
|
420
425
|
add_recent_file("pet", path)
|
|
421
426
|
reload_recent_combobox(self.recent_files_combobox, "pet")
|
|
422
427
|
|
|
428
|
+
def _audio_provider(self, geometry):
|
|
429
|
+
"""
|
|
430
|
+
寵物要跟著什麼聲音動:喇叭(跟隨牠所在螢幕的輸出裝置)或麥克風
|
|
431
|
+
(你說話時牠才動)。兩者都只讀電表數值,不擷取音訊內容。
|
|
432
|
+
What the pet reacts to: the speakers on its own screen, or the
|
|
433
|
+
microphone so it only moves while you talk. Both read a meter value and
|
|
434
|
+
capture no audio content.
|
|
435
|
+
"""
|
|
436
|
+
if self.audio_source_combobox.currentData() == "microphone":
|
|
437
|
+
return microphone_level
|
|
438
|
+
# 跟隨寵物所在螢幕的音源(比對不到就用系統預設輸出裝置)
|
|
439
|
+
# Follow the audio of the screen the pet lives on; default endpoint otherwise.
|
|
440
|
+
return audio_level_provider_for_screen(geometry[1] if geometry is not None else None)
|
|
441
|
+
|
|
423
442
|
def get_state(self) -> dict:
|
|
424
443
|
return {
|
|
425
444
|
"pet_image_path": self.pet_image_path,
|
|
@@ -433,6 +452,7 @@ class PetSettingUI(QWidget):
|
|
|
433
452
|
"target_monitor": self.target_monitor_combobox.currentText(),
|
|
434
453
|
"volume": self.volume_combobox.currentText(),
|
|
435
454
|
"audio_react": self.audio_react_checkbox.isChecked(),
|
|
455
|
+
"audio_source": self.audio_source_combobox.currentData(),
|
|
436
456
|
"tag": self.tag_checkbox.isChecked(),
|
|
437
457
|
"speech": self.speech_checkbox.isChecked(),
|
|
438
458
|
"chat": self.chat_checkbox.isChecked(),
|
|
@@ -444,6 +464,7 @@ class PetSettingUI(QWidget):
|
|
|
444
464
|
"""把預設集的內容還原到這一頁的控制項上。"""
|
|
445
465
|
self._restore_media(state)
|
|
446
466
|
apply_combobox_data_state(self.behaviour_combobox, self._behaviour_from(state))
|
|
467
|
+
apply_combobox_data_state(self.audio_source_combobox, state.get("audio_source"))
|
|
447
468
|
apply_combobox_text_state(state, (
|
|
448
469
|
(self.size_combobox, "size"),
|
|
449
470
|
(self.speed_combobox, "speed"),
|
|
@@ -11,9 +11,11 @@ from typing import List
|
|
|
11
11
|
from PySide6.QtCore import Qt
|
|
12
12
|
from PySide6.QtGui import QGuiApplication
|
|
13
13
|
from PySide6.QtWidgets import (
|
|
14
|
+
QFileDialog,
|
|
14
15
|
QCheckBox, QComboBox, QGridLayout, QLabel, QPushButton, QSlider, QWidget,
|
|
15
16
|
)
|
|
16
17
|
|
|
18
|
+
from frontengine.show.canvas.whiteboard_widget import WhiteboardWidget
|
|
17
19
|
from frontengine.show.presentation.annotation_overlay import (
|
|
18
20
|
PEN_COLORS, TOOL_ERASER, TOOL_HIGHLIGHTER, TOOL_PEN, AnnotationOverlay,
|
|
19
21
|
)
|
|
@@ -36,6 +38,7 @@ class PresentationSettingUI(QWidget):
|
|
|
36
38
|
self.grid_layout.setContentsMargins(0, 0, 0, 0)
|
|
37
39
|
|
|
38
40
|
self.annotation_widget_list: List[AnnotationOverlay] = []
|
|
41
|
+
self.whiteboard_widget_list: List[WhiteboardWidget] = []
|
|
39
42
|
self.cursor_widget_list: List[CursorEffectWidget] = []
|
|
40
43
|
self.keystroke_widget_list: List[KeystrokeDisplayWidget] = []
|
|
41
44
|
self.magnifier_widget_list: List[MagnifierWidget] = []
|
|
@@ -109,6 +112,12 @@ class PresentationSettingUI(QWidget):
|
|
|
109
112
|
self.magnifier_button.clicked.connect(self.toggle_magnifier)
|
|
110
113
|
|
|
111
114
|
# Shared
|
|
115
|
+
self.whiteboard_button = QPushButton(
|
|
116
|
+
language_wrapper.language_word_dict.get("whiteboard_start", "Whiteboard"))
|
|
117
|
+
self.whiteboard_button.clicked.connect(self.toggle_whiteboard)
|
|
118
|
+
self.whiteboard_save_button = QPushButton(
|
|
119
|
+
language_wrapper.language_word_dict.get("whiteboard_save", "Save whiteboard"))
|
|
120
|
+
self.whiteboard_save_button.clicked.connect(self.save_whiteboard)
|
|
112
121
|
self.target_monitor_label = QLabel(
|
|
113
122
|
language_wrapper.language_word_dict.get("target_monitor_label", "Target monitor"))
|
|
114
123
|
self.target_monitor_combobox = build_target_monitor_combobox()
|
|
@@ -136,9 +145,11 @@ class PresentationSettingUI(QWidget):
|
|
|
136
145
|
self.grid_layout.addWidget(self.magnifier_label, 6, 0)
|
|
137
146
|
self.grid_layout.addWidget(self.magnifier_zoom_combobox, 6, 1)
|
|
138
147
|
self.grid_layout.addWidget(self.magnifier_button, 6, 2)
|
|
139
|
-
self.grid_layout.addWidget(self.
|
|
140
|
-
self.grid_layout.addWidget(self.
|
|
141
|
-
self.grid_layout.addWidget(self.
|
|
148
|
+
self.grid_layout.addWidget(self.whiteboard_button, 7, 0)
|
|
149
|
+
self.grid_layout.addWidget(self.whiteboard_save_button, 7, 1)
|
|
150
|
+
self.grid_layout.addWidget(self.target_monitor_label, 8, 0)
|
|
151
|
+
self.grid_layout.addWidget(self.target_monitor_combobox, 8, 1)
|
|
152
|
+
self.grid_layout.addWidget(self.hint_label, 9, 0, 1, 3)
|
|
142
153
|
|
|
143
154
|
# --- shared ----------------------------------------------------------
|
|
144
155
|
def target_screen(self):
|
|
@@ -331,6 +342,41 @@ class PresentationSettingUI(QWidget):
|
|
|
331
342
|
self.magnifier_widget_list.remove(widget)
|
|
332
343
|
|
|
333
344
|
# --- preset state ----------------------------------------------------
|
|
345
|
+
# --- whiteboard ------------------------------------------------------
|
|
346
|
+
def toggle_whiteboard(self) -> None:
|
|
347
|
+
if self.whiteboard_widget_list:
|
|
348
|
+
self.stop_whiteboard()
|
|
349
|
+
else:
|
|
350
|
+
self.start_whiteboard()
|
|
351
|
+
|
|
352
|
+
def start_whiteboard(self) -> None:
|
|
353
|
+
"""開一塊無限畫布(可平移縮放,畫過的東西留在畫布座標上)。"""
|
|
354
|
+
front_engine_logger.info("[PresentationSettingUI] start_whiteboard")
|
|
355
|
+
board = WhiteboardWidget(self.annotate_color_combobox.currentData(),
|
|
356
|
+
self.annotate_width_slider.value())
|
|
357
|
+
self._present(board)
|
|
358
|
+
self.whiteboard_widget_list.append(board)
|
|
359
|
+
self.whiteboard_button.setText(
|
|
360
|
+
language_wrapper.language_word_dict.get("whiteboard_stop", "Close whiteboard"))
|
|
361
|
+
|
|
362
|
+
def stop_whiteboard(self) -> None:
|
|
363
|
+
self._close_all(self.whiteboard_widget_list)
|
|
364
|
+
self.whiteboard_button.setText(
|
|
365
|
+
language_wrapper.language_word_dict.get("whiteboard_start", "Whiteboard"))
|
|
366
|
+
|
|
367
|
+
def save_whiteboard(self):
|
|
368
|
+
"""把白板存成圖片;沒畫東西就不存。"""
|
|
369
|
+
for board in self.whiteboard_widget_list[:]:
|
|
370
|
+
try:
|
|
371
|
+
target = QFileDialog.getSaveFileName(
|
|
372
|
+
self, language_wrapper.language_word_dict.get(
|
|
373
|
+
"whiteboard_save", "Save whiteboard"), "whiteboard.png", "PNG (*.png)")[0]
|
|
374
|
+
if target:
|
|
375
|
+
return board.save_image(target)
|
|
376
|
+
except RuntimeError:
|
|
377
|
+
self.whiteboard_widget_list.remove(board)
|
|
378
|
+
return None
|
|
379
|
+
|
|
334
380
|
def get_state(self) -> dict:
|
|
335
381
|
return {
|
|
336
382
|
"annotate_tool": self.annotate_tool_combobox.currentData(),
|
|
@@ -54,6 +54,9 @@ user_setting_dict: Dict[str, Any] = {
|
|
|
54
54
|
# Sticky note contents and positions, and whether to reopen them on launch.
|
|
55
55
|
"sticky_notes": [],
|
|
56
56
|
"sticky_notes_restore": False,
|
|
57
|
+
# 看板模式:依序輪播預設集(預設關閉)
|
|
58
|
+
# Signage mode: rotate through presets; off by default.
|
|
59
|
+
"signage": {"enabled": False, "presets": [], "interval_minutes": 5, "hide_window": True},
|
|
57
60
|
# 手機遙控(會開一個本機網路埠,預設關閉)與 MIDI 綁定
|
|
58
61
|
# The phone remote opens a local network port and is off by default;
|
|
59
62
|
# plus the MIDI control bindings.
|