frontengine 1.0.2__tar.gz → 1.0.4__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.
Files changed (137) hide show
  1. {frontengine-1.0.2 → frontengine-1.0.4}/PKG-INFO +1 -1
  2. frontengine-1.0.4/frontengine/show/pet/desktop_pet.py +375 -0
  3. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/pet/pet_setting_ui.py +44 -19
  4. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/english.py +6 -1
  5. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/traditional_chinese.py +6 -1
  6. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine.egg-info/PKG-INFO +1 -1
  7. {frontengine-1.0.2 → frontengine-1.0.4}/pyproject.toml +1 -1
  8. frontengine-1.0.2/frontengine/show/pet/desktop_pet.py +0 -178
  9. {frontengine-1.0.2 → frontengine-1.0.4}/LICENSE +0 -0
  10. {frontengine-1.0.2 → frontengine-1.0.4}/README.md +0 -0
  11. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/__init__.py +0 -0
  12. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/__main__.py +0 -0
  13. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/__init__.py +0 -0
  14. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/base_widget.py +0 -0
  15. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/gif/__init__.py +0 -0
  16. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/gif/paint_gif.py +0 -0
  17. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/image/__init__.py +0 -0
  18. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/image/paint_image.py +0 -0
  19. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/load/__init__.py +0 -0
  20. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/load/load_someone_make_ui.py +0 -0
  21. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/overlay_factory.py +0 -0
  22. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/particle/__init__.py +0 -0
  23. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/particle/particle_ui.py +0 -0
  24. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/pet/__init__.py +0 -0
  25. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/scene/__init__.py +0 -0
  26. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  27. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/scene/extend_graphic_view.py +0 -0
  28. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/scene/scene.py +0 -0
  29. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/sound_player/__init__.py +0 -0
  30. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/sound_player/sound_effect.py +0 -0
  31. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/sound_player/sound_player.py +0 -0
  32. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/text/__init__.py +0 -0
  33. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/text/draw_text.py +0 -0
  34. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/video/__init__.py +0 -0
  35. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/video/video_player.py +0 -0
  36. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/web/__init__.py +0 -0
  37. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/web/webview.py +0 -0
  38. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/show/window_helpers.py +0 -0
  39. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/system_tray/__init__.py +0 -0
  40. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/system_tray/extend_system_tray.py +0 -0
  41. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/__init__.py +0 -0
  42. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/color/__init__.py +0 -0
  43. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/color/global_color.py +0 -0
  44. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/dialog/__init__.py +0 -0
  45. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  46. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  47. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  48. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/main_ui.py +0 -0
  49. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/menu/__init__.py +0 -0
  50. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/menu/help_menu.py +0 -0
  51. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/menu/how_to_menu.py +0 -0
  52. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/menu/language_menu.py +0 -0
  53. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/menu/preset_menu.py +0 -0
  54. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/menu/settings_menu.py +0 -0
  55. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/__init__.py +0 -0
  56. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/control_center/__init__.py +0 -0
  57. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  58. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/gif/__init__.py +0 -0
  59. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  60. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/image/__init__.py +0 -0
  61. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  62. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/particle/__init__.py +0 -0
  63. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  64. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/pet/__init__.py +0 -0
  65. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  66. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  67. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  68. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  69. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  70. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  71. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  72. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  73. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  74. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  75. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  76. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  77. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/sound_player/__init__.py +0 -0
  78. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  79. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/text/__init__.py +0 -0
  80. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  81. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/utils.py +0 -0
  82. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/video/__init__.py +0 -0
  83. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  84. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/web/__init__.py +0 -0
  85. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  86. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/user_setting/__init__.py +0 -0
  87. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/user_setting/preset_repository.py +0 -0
  88. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/user_setting/scene_setting.py +0 -0
  89. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/user_setting/user_setting_file.py +0 -0
  90. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/__init__.py +0 -0
  91. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/browser/__init__.py +0 -0
  92. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/browser/browser.py +0 -0
  93. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/critical_exit/__init__.py +0 -0
  94. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  95. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  96. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  97. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/exception/__init__.py +0 -0
  98. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/exception/exception_tags.py +0 -0
  99. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/exception/exceptions.py +0 -0
  100. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/file/__init__.py +0 -0
  101. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/file/open/__init__.py +0 -0
  102. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/file/open/open_file.py +0 -0
  103. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/file/save/__init__.py +0 -0
  104. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/file/save/save_file.py +0 -0
  105. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/hotkey/__init__.py +0 -0
  106. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  107. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/json/__init__.py +0 -0
  108. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/json/json_file.py +0 -0
  109. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/json/json_repository.py +0 -0
  110. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/json_format/__init__.py +0 -0
  111. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/json_format/json_process.py +0 -0
  112. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/logging/__init__.py +0 -0
  113. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/logging/loggin_instance.py +0 -0
  114. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/__init__.py +0 -0
  115. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/france.py +0 -0
  116. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/germany.py +0 -0
  117. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/italy.py +0 -0
  118. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  119. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/russian.py +0 -0
  120. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  121. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/preset_schedule/__init__.py +0 -0
  122. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  123. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/redirect_manager/__init__.py +0 -0
  124. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  125. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/smart_pause/__init__.py +0 -0
  126. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  127. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/theme_schedule/__init__.py +0 -0
  128. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  129. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/utils/web_url.py +0 -0
  130. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/worker/__init__.py +0 -0
  131. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine/worker/qthread_worker.py +0 -0
  132. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine.egg-info/SOURCES.txt +0 -0
  133. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine.egg-info/dependency_links.txt +0 -0
  134. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine.egg-info/entry_points.txt +0 -0
  135. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine.egg-info/requires.txt +0 -0
  136. {frontengine-1.0.2 → frontengine-1.0.4}/frontengine.egg-info/top_level.txt +0 -0
  137. {frontengine-1.0.2 → frontengine-1.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.2
3
+ Version: 1.0.4
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,375 @@
1
+ import random as _random_module
2
+ from pathlib import Path
3
+ from typing import Optional, Tuple
4
+
5
+ from PySide6.QtCore import Qt, QRect, QTimer, Signal
6
+ from PySide6.QtGui import QAction, QCursor, QMovie, QPainter, QPixmap, QTransform
7
+ from PySide6.QtWidgets import QMenu, QMessageBox
8
+
9
+ from frontengine.show.base_widget import BaseWidget
10
+ from frontengine.show.window_helpers import apply_overlay_window_flags
11
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
12
+ from frontengine.utils.multi_language.language_wrapper import language_wrapper
13
+
14
+ # 行為模式 / Behaviour modes
15
+ BEHAVIOUR_FLOOR = "floor" # walk on the floor with gravity, throw & bounce
16
+ BEHAVIOUR_WANDER = "wander" # free 2D bounce, no gravity
17
+ BEHAVIOUR_CHASE = "chase" # chase the mouse cursor, sleep when caught
18
+
19
+ # 攀爬時所在的表面 / Surface the pet is crawling on while climbing
20
+ SURFACE_FLOOR = "floor"
21
+ SURFACE_LEFT = "left"
22
+ SURFACE_CEILING = "ceiling"
23
+ SURFACE_RIGHT = "right"
24
+
25
+
26
+ class PetMotion:
27
+ """
28
+ 純邏輯的寵物移動模型(不依賴 Qt,完全可測試)。包含:
29
+ - 重力落下、落地、彈跳(受動量丟出後)
30
+ - 走路/發呆的隨機行為狀態機(rng 可注入以利測試)
31
+ - 追逐游標模式(追到後進入睡眠,游標移動即醒)
32
+ A pure, Qt-free motion model: gravity fall/land/bounce (after a throw),
33
+ a random walk/idle behaviour state machine (injectable rng), and a
34
+ cursor-chase mode that sleeps once it catches the target.
35
+ """
36
+
37
+ GRAVITY = 2.0
38
+ BOUNCE_DAMPING = 0.55
39
+ WALL_DAMPING = 0.6
40
+ BOUNCE_THRESHOLD = 8.0
41
+ CHASE_STOP_DISTANCE = 10.0
42
+ CHASE_SPEED_FACTOR = 2.0
43
+ CLIMB_CHANCE = 0.5 # chance to grab a wall (vs. turn around) at a floor corner
44
+ DROP_CHANCE = 0.008 # chance per step to let go while on a wall/ceiling
45
+
46
+ def __init__(
47
+ self,
48
+ x: int,
49
+ y: int,
50
+ width: int,
51
+ height: int,
52
+ bounds: Tuple[int, int, int, int],
53
+ speed: int = 3,
54
+ behaviour: str = BEHAVIOUR_FLOOR,
55
+ rng=None,
56
+ climb: bool = True,
57
+ ) -> None:
58
+ self.x = float(x)
59
+ self.y = float(y)
60
+ self.width = int(width)
61
+ self.height = int(height)
62
+ self.bounds = bounds # (left, top, right, bottom)
63
+ self.speed = max(1, int(speed))
64
+ self.behaviour = behaviour
65
+ self._rng = rng if rng is not None else _random_module.SystemRandom()
66
+ self.vx = float(self.speed)
67
+ self.vy = 0.0 if behaviour != BEHAVIOUR_WANDER else float(self.speed)
68
+ self._airborne = False
69
+ self.state = "walk" # walk | idle
70
+ self._state_ticks = 0
71
+ self.target: Optional[Tuple[float, float]] = None
72
+ self.asleep = False
73
+ self.climb = bool(climb)
74
+ self.surface = SURFACE_FLOOR
75
+
76
+ @property
77
+ def facing_left(self) -> bool:
78
+ return self.vx < 0
79
+
80
+ def set_bounds(self, bounds: Tuple[int, int, int, int]) -> None:
81
+ self.bounds = bounds
82
+
83
+ def set_target(self, x: float, y: float) -> None:
84
+ self.target = (float(x), float(y))
85
+
86
+ def throw(self, vx: float, vy: float) -> None:
87
+ """拖曳放開後以動量丟出(只在重力模式有意義)。"""
88
+ self.vx = float(vx)
89
+ self.vy = float(vy)
90
+ self._airborne = True
91
+ self.asleep = False
92
+ self.surface = SURFACE_FLOOR
93
+
94
+ def step(self) -> Tuple[int, int]:
95
+ left, top, right, bottom = self.bounds
96
+ floor_y = bottom - self.height
97
+ if self.behaviour == BEHAVIOUR_CHASE:
98
+ return self._step_chase(left, top, right, bottom)
99
+ if self.behaviour == BEHAVIOUR_WANDER:
100
+ return self._step_wander(left, top, right, bottom)
101
+ return self._step_floor(left, top, right, bottom, floor_y)
102
+
103
+ # --- floor / gravity / climbing ---
104
+ def _step_floor(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
105
+ airborne = self._airborne or (self.surface == SURFACE_FLOOR and self.y < floor_y - 0.5)
106
+ if airborne:
107
+ self.vy += self.GRAVITY
108
+ self.x += self.vx
109
+ self.y += self.vy
110
+ if self.x <= left:
111
+ self.x = float(left)
112
+ self.vx = abs(self.vx) * self.WALL_DAMPING
113
+ elif self.x + self.width >= right:
114
+ self.x = float(right - self.width)
115
+ self.vx = -abs(self.vx) * self.WALL_DAMPING
116
+ if self.y <= top:
117
+ self.y = float(top)
118
+ self.vy = abs(self.vy) * self.WALL_DAMPING
119
+ if self.y >= floor_y:
120
+ self.y = float(floor_y)
121
+ if abs(self.vy) > self.BOUNCE_THRESHOLD:
122
+ self.vy = -self.vy * self.BOUNCE_DAMPING
123
+ else:
124
+ self.vy = 0.0
125
+ self._airborne = False
126
+ self.surface = SURFACE_FLOOR
127
+ self.vx = float(self.speed) if self.vx >= 0 else float(-self.speed)
128
+ self._new_state(force_walk=True)
129
+ return int(self.x), int(self.y)
130
+
131
+ if self.surface != SURFACE_FLOOR:
132
+ return self._step_climb(left, top, right, bottom, floor_y)
133
+
134
+ # grounded on the floor: random walk / idle, with a chance to climb a wall
135
+ self.y = float(floor_y)
136
+ if self._state_ticks <= 0:
137
+ self._new_state()
138
+ self._state_ticks -= 1
139
+ if self.state != "walk":
140
+ return int(self.x), int(self.y)
141
+ self.x += self.vx
142
+ if self.x <= left:
143
+ self.x = float(left)
144
+ if self.climb and self._rng.random() < self.CLIMB_CHANCE:
145
+ self.surface = SURFACE_LEFT
146
+ self.vy = float(-self.speed)
147
+ else:
148
+ self.vx = abs(self.vx)
149
+ elif self.x + self.width >= right:
150
+ self.x = float(right - self.width)
151
+ if self.climb and self._rng.random() < self.CLIMB_CHANCE:
152
+ self.surface = SURFACE_RIGHT
153
+ self.vy = float(-self.speed)
154
+ else:
155
+ self.vx = -abs(self.vx)
156
+ return int(self.x), int(self.y)
157
+
158
+ def _step_climb(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
159
+ """沿螢幕邊緣爬行;隨機放手則掉落 / Crawl the screen perimeter; may let go and fall."""
160
+ if self._rng.random() < self.DROP_CHANCE:
161
+ self.surface = SURFACE_FLOOR
162
+ self._airborne = True
163
+ self.vy = 1.0
164
+ return int(self.x), int(self.y)
165
+
166
+ if self.surface == SURFACE_RIGHT:
167
+ self.x = float(right - self.width)
168
+ self.y += self.vy
169
+ if self.vy < 0 and self.y <= top: # reached the ceiling, crawl left
170
+ self.y = float(top)
171
+ self.surface = SURFACE_CEILING
172
+ self.vx = float(-self.speed)
173
+ elif self.vy > 0 and self.y >= floor_y: # climbed back down
174
+ self.y = float(floor_y)
175
+ self.surface = SURFACE_FLOOR
176
+ self.vx = float(-self.speed)
177
+ self._new_state(force_walk=True)
178
+ elif self.surface == SURFACE_LEFT:
179
+ self.x = float(left)
180
+ self.y += self.vy
181
+ if self.vy < 0 and self.y <= top:
182
+ self.y = float(top)
183
+ self.surface = SURFACE_CEILING
184
+ self.vx = float(self.speed)
185
+ elif self.vy > 0 and self.y >= floor_y:
186
+ self.y = float(floor_y)
187
+ self.surface = SURFACE_FLOOR
188
+ self.vx = float(self.speed)
189
+ self._new_state(force_walk=True)
190
+ elif self.surface == SURFACE_CEILING:
191
+ self.y = float(top)
192
+ self.x += self.vx
193
+ if self.vx < 0 and self.x <= left: # corner -> climb down the left wall
194
+ self.x = float(left)
195
+ self.surface = SURFACE_LEFT
196
+ self.vy = float(self.speed)
197
+ elif self.vx > 0 and self.x + self.width >= right:
198
+ self.x = float(right - self.width)
199
+ self.surface = SURFACE_RIGHT
200
+ self.vy = float(self.speed)
201
+ return int(self.x), int(self.y)
202
+
203
+ def _new_state(self, force_walk: bool = False) -> None:
204
+ if force_walk or self._rng.random() < 0.65:
205
+ self.state = "walk"
206
+ if not force_walk:
207
+ self.vx = float(self.speed) if self._rng.random() < 0.5 else float(-self.speed)
208
+ self._state_ticks = self._rng.randint(30, 120)
209
+ else:
210
+ self.state = "idle"
211
+ self._state_ticks = self._rng.randint(20, 80)
212
+
213
+ # --- free wander ---
214
+ def _step_wander(self, left, top, right, bottom) -> Tuple[int, int]:
215
+ if abs(self.vy) < 1e-9:
216
+ self.vy = float(self.speed)
217
+ self.x += self.vx
218
+ self.y += self.vy
219
+ if self.x <= left:
220
+ self.x = float(left)
221
+ self.vx = abs(self.vx)
222
+ elif self.x + self.width >= right:
223
+ self.x = float(right - self.width)
224
+ self.vx = -abs(self.vx)
225
+ if self.y <= top:
226
+ self.y = float(top)
227
+ self.vy = abs(self.vy)
228
+ elif self.y + self.height >= bottom:
229
+ self.y = float(bottom - self.height)
230
+ self.vy = -abs(self.vy)
231
+ return int(self.x), int(self.y)
232
+
233
+ # --- chase cursor ---
234
+ def _step_chase(self, left, top, right, bottom) -> Tuple[int, int]:
235
+ if self.target is None:
236
+ return int(self.x), int(self.y)
237
+ target_x, target_y = self.target
238
+ dx = target_x - (self.x + self.width / 2)
239
+ dy = target_y - (self.y + self.height / 2)
240
+ distance = (dx * dx + dy * dy) ** 0.5
241
+ if distance <= self.CHASE_STOP_DISTANCE:
242
+ self.asleep = True
243
+ return int(self.x), int(self.y)
244
+ self.asleep = False
245
+ step = float(self.speed) * self.CHASE_SPEED_FACTOR
246
+ self.x += step * (dx / distance)
247
+ self.y += step * (dy / distance)
248
+ self.vx = abs(self.vx) if dx >= 0 else -abs(self.vx)
249
+ self.x = min(max(self.x, float(left)), float(right - self.width))
250
+ self.y = min(max(self.y, float(top)), float(bottom - self.height))
251
+ return int(self.x), int(self.y)
252
+
253
+
254
+ class DesktopPetWidget(BaseWidget):
255
+ """
256
+ 桌面寵物:可自主走動、可拖曳丟出、可追游標的動畫精靈。支援 GIF 與靜態圖。
257
+ A draggable, throwable, cursor-chasing animated sprite (GIF or image).
258
+ """
259
+
260
+ clone_requested = Signal()
261
+
262
+ def __init__(self, image_path: str, size: int = 128, speed: int = 3,
263
+ behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True):
264
+ front_engine_logger.info(
265
+ f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, "
266
+ f"behaviour={behaviour}, climb={climb}"
267
+ )
268
+ super().__init__()
269
+ self.opacity = 1.0
270
+ self.pet_size: int = max(16, int(size))
271
+ self.image_path: Path = Path(image_path)
272
+ self.movie: Optional[QMovie] = None
273
+ self.pixmap: Optional[QPixmap] = None
274
+ self._dragging: bool = False
275
+ self._drag_offset = None
276
+ self._last_move_delta: Tuple[int, int] = (0, 0)
277
+
278
+ if self.image_path.exists() and self.image_path.is_file():
279
+ if self.image_path.suffix.lower() in (".gif", ".webp"):
280
+ self.movie = QMovie(str(self.image_path))
281
+ self.movie.frameChanged.connect(self.update)
282
+ self.movie.start()
283
+ else:
284
+ self.pixmap = QPixmap(str(self.image_path))
285
+ else:
286
+ front_engine_logger.error(f"[DesktopPetWidget] File not found: {self.image_path}")
287
+ message_box = QMessageBox(self)
288
+ message_box.setText(language_wrapper.language_word_dict.get("pet_message_box_text", "Pet image not found"))
289
+ message_box.show()
290
+
291
+ self.resize(self.pet_size, self.pet_size)
292
+ self.motion = PetMotion(
293
+ 0, 0, self.pet_size, self.pet_size, (0, 0, 1920, 1080), speed, behaviour, climb=climb
294
+ )
295
+
296
+ self._timer = QTimer(self)
297
+ self._timer.timeout.connect(self._on_tick)
298
+
299
+ self.menu = QMenu(self)
300
+ self.clone_action = QAction(language_wrapper.language_word_dict.get("pet_clone", "Clone"), self)
301
+ self.clone_action.triggered.connect(self.clone_requested.emit)
302
+ self.menu.addAction(self.clone_action)
303
+ self.close_action = QAction(language_wrapper.language_word_dict.get("control_center_close_all", "Close"), self)
304
+ self.close_action.triggered.connect(self.close)
305
+ self.menu.addAction(self.close_action)
306
+
307
+ def set_pet_window_flag(self) -> None:
308
+ """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂。"""
309
+ apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
310
+
311
+ def _current_pixmap(self) -> Optional[QPixmap]:
312
+ if self.movie is not None:
313
+ return self.movie.currentPixmap()
314
+ return self.pixmap
315
+
316
+ def draw_content(self, painter: QPainter) -> None:
317
+ pixmap = self._current_pixmap()
318
+ if pixmap is None or pixmap.isNull():
319
+ return
320
+ if self.motion.facing_left:
321
+ pixmap = pixmap.transformed(QTransform().scale(-1, 1))
322
+ painter.drawPixmap(QRect(0, 0, self.width(), self.height()), pixmap)
323
+
324
+ def start_moving(self, bounds: Tuple[int, int, int, int], interval_ms: int = 33) -> None:
325
+ front_engine_logger.info(f"[DesktopPetWidget] start_moving | bounds={bounds}")
326
+ left, top, right, bottom = bounds
327
+ self.motion.set_bounds(bounds)
328
+ self.motion.x = float(left)
329
+ self.motion.y = float(bottom - self.pet_size)
330
+ self.move(int(self.motion.x), int(self.motion.y))
331
+ self._timer.start(max(10, int(interval_ms)))
332
+
333
+ def _on_tick(self) -> None:
334
+ if self._dragging:
335
+ return
336
+ if self.motion.behaviour == BEHAVIOUR_CHASE:
337
+ cursor = QCursor.pos()
338
+ self.motion.set_target(cursor.x(), cursor.y())
339
+ x, y = self.motion.step()
340
+ self.move(x, y)
341
+
342
+ # --- dragging + throwing ---
343
+ def mousePressEvent(self, event) -> None:
344
+ if event.button() == Qt.MouseButton.LeftButton:
345
+ self._dragging = True
346
+ self._last_move_delta = (0, 0)
347
+ self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
348
+ super().mousePressEvent(event)
349
+
350
+ def mouseMoveEvent(self, event) -> None:
351
+ if self._dragging and self._drag_offset is not None:
352
+ new_top_left = event.globalPosition().toPoint() - self._drag_offset
353
+ self._last_move_delta = (new_top_left.x() - self.x(), new_top_left.y() - self.y())
354
+ self.move(new_top_left)
355
+ super().mouseMoveEvent(event)
356
+
357
+ def mouseReleaseEvent(self, event) -> None:
358
+ if self._dragging:
359
+ self._dragging = False
360
+ self.motion.x = float(self.x())
361
+ self.motion.y = float(self.y())
362
+ # Fling with the momentum of the last drag movement (floor mode).
363
+ if self.motion.behaviour == BEHAVIOUR_FLOOR:
364
+ self.motion.throw(self._last_move_delta[0], self._last_move_delta[1])
365
+ super().mouseReleaseEvent(event)
366
+
367
+ def contextMenuEvent(self, event) -> None:
368
+ self.menu.popup(event.globalPos())
369
+
370
+ def closeEvent(self, event) -> None:
371
+ if self._timer.isActive():
372
+ self._timer.stop()
373
+ if self.movie is not None:
374
+ self.movie.stop()
375
+ super().closeEvent(event)
@@ -2,10 +2,12 @@ from typing import Optional
2
2
 
3
3
  from PySide6.QtGui import QGuiApplication
4
4
  from PySide6.QtWidgets import (
5
- QWidget, QGridLayout, QLabel, QPushButton, QMessageBox, QCheckBox, QComboBox,
5
+ QWidget, QGridLayout, QLabel, QPushButton, QMessageBox, QComboBox, QCheckBox,
6
6
  )
7
7
 
8
- from frontengine.show.pet.desktop_pet import DesktopPetWidget
8
+ from frontengine.show.pet.desktop_pet import (
9
+ DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE,
10
+ )
9
11
  from frontengine.ui.dialog.choose_file_dialog import choose_pet
10
12
  from frontengine.ui.page.utils import (
11
13
  build_recent_combobox,
@@ -50,9 +52,19 @@ class PetSettingUI(QWidget):
50
52
  self.speed_combobox.addItems([str(n) for n in range(1, 11)])
51
53
  self.speed_combobox.setCurrentText("3")
52
54
 
53
- # Gravity / walk on floor
54
- self.gravity_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_gravity_label", "Walk on floor"))
55
- self.gravity_checkbox.setChecked(True)
55
+ # Behaviour
56
+ self.behaviour_label = QLabel(language_wrapper.language_word_dict.get("pet_behaviour_label", "Behaviour"))
57
+ self.behaviour_combobox = QComboBox()
58
+ self.behaviour_combobox.addItem(
59
+ language_wrapper.language_word_dict.get("pet_behaviour_floor", "Walk on floor"), BEHAVIOUR_FLOOR)
60
+ self.behaviour_combobox.addItem(
61
+ language_wrapper.language_word_dict.get("pet_behaviour_wander", "Free wander"), BEHAVIOUR_WANDER)
62
+ self.behaviour_combobox.addItem(
63
+ language_wrapper.language_word_dict.get("pet_behaviour_chase", "Chase cursor"), BEHAVIOUR_CHASE)
64
+
65
+ # Climb walls / ceiling (only meaningful in floor mode)
66
+ self.climb_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_climb_label", "Climb walls"))
67
+ self.climb_checkbox.setChecked(True)
56
68
 
57
69
  # Start
58
70
  self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
@@ -73,21 +85,32 @@ class PetSettingUI(QWidget):
73
85
  self.grid_layout.addWidget(self.size_combobox, 1, 1)
74
86
  self.grid_layout.addWidget(self.speed_label, 2, 0)
75
87
  self.grid_layout.addWidget(self.speed_combobox, 2, 1)
76
- self.grid_layout.addWidget(self.gravity_checkbox, 3, 0)
88
+ self.grid_layout.addWidget(self.behaviour_label, 3, 0)
89
+ self.grid_layout.addWidget(self.behaviour_combobox, 3, 1)
90
+ self.grid_layout.addWidget(self.climb_checkbox, 3, 2)
77
91
  self.grid_layout.addWidget(self.start_button, 4, 0)
78
92
  self.grid_layout.addWidget(self.recent_files_label, 5, 0)
79
93
  self.grid_layout.addWidget(self.recent_files_combobox, 5, 1)
80
94
 
81
- def _create_pet_widget(self) -> DesktopPetWidget:
95
+ def _spawn_pet(self) -> None:
96
+ """建立、顯示並開始移動一隻寵物(供 Start 與右鍵複製共用)。"""
97
+ if not self.pet_image_path:
98
+ return
82
99
  pet = DesktopPetWidget(
83
100
  image_path=self.pet_image_path,
84
101
  size=int(self.size_combobox.currentText()),
85
102
  speed=int(self.speed_combobox.currentText()),
86
- gravity=self.gravity_checkbox.isChecked(),
103
+ behaviour=self.behaviour_combobox.currentData(),
104
+ climb=self.climb_checkbox.isChecked(),
87
105
  )
106
+ pet.clone_requested.connect(self._spawn_pet)
88
107
  pet.set_pet_window_flag()
89
108
  self.pet_list.append(pet)
90
- return pet
109
+ screen = QGuiApplication.primaryScreen()
110
+ geometry = screen.availableGeometry() if screen is not None else None
111
+ pet.show()
112
+ if geometry is not None:
113
+ pet.start_moving((geometry.left(), geometry.top(), geometry.right(), geometry.bottom()))
91
114
 
92
115
  def start_play_pet(self) -> None:
93
116
  front_engine_logger.info("[PetSettingUI] start_play_pet")
@@ -96,13 +119,7 @@ class PetSettingUI(QWidget):
96
119
  message_box.setText(language_wrapper.language_word_dict.get("not_prepare"))
97
120
  message_box.exec()
98
121
  return
99
- pet = self._create_pet_widget()
100
- screen = QGuiApplication.primaryScreen()
101
- geometry = screen.availableGeometry() if screen is not None else None
102
- pet.show()
103
- if geometry is not None:
104
- bounds = (geometry.left(), geometry.top(), geometry.right(), geometry.bottom())
105
- pet.start_moving(bounds)
122
+ self._spawn_pet()
106
123
 
107
124
  def choose_and_play(self) -> None:
108
125
  front_engine_logger.info("[PetSettingUI] choose_and_play")
@@ -137,7 +154,8 @@ class PetSettingUI(QWidget):
137
154
  "pet_image_path": self.pet_image_path,
138
155
  "size": self.size_combobox.currentText(),
139
156
  "speed": self.speed_combobox.currentText(),
140
- "gravity": self.gravity_checkbox.isChecked(),
157
+ "behaviour": self.behaviour_combobox.currentData(),
158
+ "climb": self.climb_checkbox.isChecked(),
141
159
  }
142
160
 
143
161
  def set_state(self, state: dict) -> None:
@@ -150,5 +168,12 @@ class PetSettingUI(QWidget):
150
168
  index = combobox.findText(str(state[key]))
151
169
  if index >= 0:
152
170
  combobox.setCurrentIndex(index)
153
- if "gravity" in state:
154
- self.gravity_checkbox.setChecked(bool(state["gravity"]))
171
+ behaviour = state.get("behaviour")
172
+ if behaviour is None and "gravity" in state: # back-compat with old presets
173
+ behaviour = BEHAVIOUR_FLOOR if state.get("gravity") else BEHAVIOUR_WANDER
174
+ if behaviour is not None:
175
+ index = self.behaviour_combobox.findData(behaviour)
176
+ if index >= 0:
177
+ self.behaviour_combobox.setCurrentIndex(index)
178
+ if "climb" in state:
179
+ self.climb_checkbox.setChecked(bool(state["climb"]))
@@ -219,7 +219,12 @@ english_word_dict = {
219
219
  "pet_choose_file": "Choose pet sprite",
220
220
  "pet_size_label": "Size",
221
221
  "pet_speed_label": "Speed",
222
- "pet_gravity_label": "Walk on floor",
222
+ "pet_behaviour_label": "Behaviour",
223
+ "pet_behaviour_floor": "Walk on floor",
224
+ "pet_behaviour_wander": "Free wander",
225
+ "pet_behaviour_chase": "Chase cursor",
226
+ "pet_climb_label": "Climb walls",
227
+ "pet_clone": "Clone",
223
228
  "pet_start": "Spawn pet",
224
229
  "pet_message_box_text": "Pet image not found",
225
230
  }
@@ -220,7 +220,12 @@ traditional_chinese_word_dict = {
220
220
  "pet_choose_file": "選擇寵物圖片",
221
221
  "pet_size_label": "大小",
222
222
  "pet_speed_label": "速度",
223
- "pet_gravity_label": "沿底部行走",
223
+ "pet_behaviour_label": "行為",
224
+ "pet_behaviour_floor": "沿底部行走",
225
+ "pet_behaviour_wander": "自由漫遊",
226
+ "pet_behaviour_chase": "追逐游標",
227
+ "pet_climb_label": "攀爬牆壁",
228
+ "pet_clone": "複製",
224
229
  "pet_start": "召喚寵物",
225
230
  "pet_message_box_text": "找不到寵物圖片",
226
231
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.2
3
+ Version: 1.0.4
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
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "frontengine"
9
- version = "1.0.2"
9
+ version = "1.0.4"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
@@ -1,178 +0,0 @@
1
- from pathlib import Path
2
- from typing import Optional, Tuple
3
-
4
- from PySide6.QtCore import Qt, QRect, QTimer
5
- from PySide6.QtGui import QAction, QMovie, QPainter, QPixmap, QTransform
6
- from PySide6.QtWidgets import QMenu, QMessageBox
7
-
8
- from frontengine.show.base_widget import BaseWidget
9
- from frontengine.show.window_helpers import apply_overlay_window_flags
10
- from frontengine.utils.logging.loggin_instance import front_engine_logger
11
- from frontengine.utils.multi_language.language_wrapper import language_wrapper
12
-
13
-
14
- class PetMotion:
15
- """
16
- 純邏輯的移動模型(不依賴 Qt,便於測試)。gravity=True 時寵物沿著底部
17
- 行走並在左右邊緣轉向;否則在四邊之間自由彈跳。
18
- Pure movement model (no Qt, easy to test). With gravity the pet walks
19
- along the floor and turns at the left/right edges; otherwise it bounces
20
- freely between all four edges.
21
- """
22
-
23
- def __init__(
24
- self,
25
- x: int,
26
- y: int,
27
- width: int,
28
- height: int,
29
- bounds: Tuple[int, int, int, int],
30
- speed: int = 3,
31
- gravity: bool = True,
32
- ) -> None:
33
- self.x = float(x)
34
- self.y = float(y)
35
- self.width = int(width)
36
- self.height = int(height)
37
- self.bounds = bounds # (left, top, right, bottom)
38
- self.speed = max(1, int(speed))
39
- self.gravity = bool(gravity)
40
- self.vx = float(self.speed)
41
- self.vy = 0.0 if gravity else float(self.speed)
42
-
43
- @property
44
- def facing_left(self) -> bool:
45
- return self.vx < 0
46
-
47
- def set_bounds(self, bounds: Tuple[int, int, int, int]) -> None:
48
- self.bounds = bounds
49
-
50
- def step(self) -> Tuple[int, int]:
51
- left, top, right, bottom = self.bounds
52
- self.x += self.vx
53
- if self.gravity:
54
- self.y = float(bottom - self.height)
55
- else:
56
- self.y += self.vy
57
- if self.y <= top:
58
- self.y = float(top)
59
- self.vy = abs(self.vy)
60
- elif self.y + self.height >= bottom:
61
- self.y = float(bottom - self.height)
62
- self.vy = -abs(self.vy)
63
- if self.x <= left:
64
- self.x = float(left)
65
- self.vx = abs(self.vx)
66
- elif self.x + self.width >= right:
67
- self.x = float(right - self.width)
68
- self.vx = -abs(self.vx)
69
- return int(self.x), int(self.y)
70
-
71
-
72
- class DesktopPetWidget(BaseWidget):
73
- """
74
- 桌面寵物:可自主走動、可拖曳的動畫精靈。支援 GIF(QMovie)與靜態圖。
75
- A draggable, self-walking animated sprite. Supports GIF (QMovie) and
76
- static images.
77
- """
78
-
79
- def __init__(self, image_path: str, size: int = 128, speed: int = 3, gravity: bool = True):
80
- front_engine_logger.info(
81
- f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, gravity={gravity}"
82
- )
83
- super().__init__()
84
- self.opacity = 1.0
85
- self.pet_size: int = max(16, int(size))
86
- self.image_path: Path = Path(image_path)
87
- self.movie: Optional[QMovie] = None
88
- self.pixmap: Optional[QPixmap] = None
89
- self._dragging: bool = False
90
- self._drag_offset = None
91
-
92
- if self.image_path.exists() and self.image_path.is_file():
93
- if self.image_path.suffix.lower() in (".gif", ".webp"):
94
- self.movie = QMovie(str(self.image_path))
95
- self.movie.frameChanged.connect(self.update)
96
- self.movie.start()
97
- else:
98
- self.pixmap = QPixmap(str(self.image_path))
99
- else:
100
- front_engine_logger.error(f"[DesktopPetWidget] File not found: {self.image_path}")
101
- message_box = QMessageBox(self)
102
- message_box.setText(language_wrapper.language_word_dict.get("pet_message_box_text", "Pet image not found"))
103
- message_box.show()
104
-
105
- self.resize(self.pet_size, self.pet_size)
106
- self.motion = PetMotion(0, 0, self.pet_size, self.pet_size, (0, 0, 1920, 1080), speed, gravity)
107
-
108
- self._timer = QTimer(self)
109
- self._timer.timeout.connect(self._on_tick)
110
-
111
- # Right-click to close
112
- self.close_action = QAction(language_wrapper.language_word_dict.get("control_center_close_all", "Close"), self)
113
- self.close_action.triggered.connect(self.close)
114
- self.menu = QMenu(self)
115
- self.menu.addAction(self.close_action)
116
-
117
- def set_pet_window_flag(self) -> None:
118
- """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂。"""
119
- apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
120
-
121
- def _current_pixmap(self) -> Optional[QPixmap]:
122
- if self.movie is not None:
123
- return self.movie.currentPixmap()
124
- return self.pixmap
125
-
126
- def draw_content(self, painter: QPainter) -> None:
127
- pixmap = self._current_pixmap()
128
- if pixmap is None or pixmap.isNull():
129
- return
130
- if self.motion.facing_left:
131
- # Mirror horizontally so the sprite faces its walking direction.
132
- pixmap = pixmap.transformed(QTransform().scale(-1, 1))
133
- painter.drawPixmap(QRect(0, 0, self.width(), self.height()), pixmap)
134
-
135
- def start_moving(self, bounds: Tuple[int, int, int, int], interval_ms: int = 33) -> None:
136
- """在指定範圍內開始走動 / Start walking within `bounds` (left, top, right, bottom)."""
137
- front_engine_logger.info(f"[DesktopPetWidget] start_moving | bounds={bounds}")
138
- left, top, right, bottom = bounds
139
- self.motion.set_bounds(bounds)
140
- self.motion.x = float(left)
141
- self.motion.y = float(bottom - self.pet_size)
142
- self.move(int(self.motion.x), int(self.motion.y))
143
- self._timer.start(max(10, int(interval_ms)))
144
-
145
- def _on_tick(self) -> None:
146
- if self._dragging:
147
- return
148
- x, y = self.motion.step()
149
- self.move(x, y)
150
-
151
- # --- dragging ---
152
- def mousePressEvent(self, event) -> None:
153
- if event.button() == Qt.MouseButton.LeftButton:
154
- self._dragging = True
155
- self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
156
- super().mousePressEvent(event)
157
-
158
- def mouseMoveEvent(self, event) -> None:
159
- if self._dragging and self._drag_offset is not None:
160
- self.move(event.globalPosition().toPoint() - self._drag_offset)
161
- super().mouseMoveEvent(event)
162
-
163
- def mouseReleaseEvent(self, event) -> None:
164
- if self._dragging:
165
- self._dragging = False
166
- self.motion.x = float(self.x())
167
- self.motion.y = float(self.y())
168
- super().mouseReleaseEvent(event)
169
-
170
- def contextMenuEvent(self, event) -> None:
171
- self.menu.popup(event.globalPos())
172
-
173
- def closeEvent(self, event) -> None:
174
- if self._timer.isActive():
175
- self._timer.stop()
176
- if self.movie is not None:
177
- self.movie.stop()
178
- super().closeEvent(event)
File without changes
File without changes
File without changes