batframework 1.0.9a7__py3-none-any.whl → 1.0.9a9__py3-none-any.whl
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.
- batFramework/__init__.py +20 -11
- batFramework/action.py +1 -1
- batFramework/animatedSprite.py +47 -116
- batFramework/animation.py +30 -5
- batFramework/audioManager.py +8 -5
- batFramework/baseScene.py +240 -0
- batFramework/camera.py +4 -0
- batFramework/constants.py +6 -2
- batFramework/cutscene.py +221 -21
- batFramework/cutsceneManager.py +5 -2
- batFramework/drawable.py +7 -5
- batFramework/easingController.py +10 -11
- batFramework/entity.py +21 -2
- batFramework/enums.py +48 -33
- batFramework/gui/__init__.py +6 -3
- batFramework/gui/animatedLabel.py +10 -2
- batFramework/gui/button.py +4 -31
- batFramework/gui/clickableWidget.py +63 -50
- batFramework/gui/constraints/constraints.py +212 -136
- batFramework/gui/container.py +77 -58
- batFramework/gui/debugger.py +12 -17
- batFramework/gui/draggableWidget.py +21 -17
- batFramework/gui/image.py +3 -10
- batFramework/gui/indicator.py +56 -1
- batFramework/gui/interactiveWidget.py +127 -108
- batFramework/gui/label.py +73 -64
- batFramework/gui/layout.py +286 -445
- batFramework/gui/meter.py +42 -20
- batFramework/gui/radioButton.py +20 -69
- batFramework/gui/root.py +99 -29
- batFramework/gui/selector.py +250 -0
- batFramework/gui/shape.py +13 -5
- batFramework/gui/slider.py +262 -107
- batFramework/gui/syncedVar.py +49 -0
- batFramework/gui/textInput.py +46 -22
- batFramework/gui/toggle.py +70 -52
- batFramework/gui/tooltip.py +30 -0
- batFramework/gui/widget.py +222 -135
- batFramework/manager.py +7 -8
- batFramework/particle.py +4 -1
- batFramework/propertyEaser.py +79 -0
- batFramework/renderGroup.py +17 -50
- batFramework/resourceManager.py +43 -13
- batFramework/scene.py +15 -335
- batFramework/sceneLayer.py +138 -0
- batFramework/sceneManager.py +31 -36
- batFramework/scrollingSprite.py +8 -3
- batFramework/sprite.py +1 -1
- batFramework/templates/__init__.py +1 -2
- batFramework/templates/controller.py +97 -0
- batFramework/timeManager.py +76 -22
- batFramework/transition.py +37 -103
- batFramework/utils.py +125 -66
- {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/METADATA +24 -3
- batframework-1.0.9a9.dist-info/RECORD +67 -0
- {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/WHEEL +1 -1
- batFramework/character.py +0 -27
- batFramework/templates/character.py +0 -43
- batFramework/templates/states.py +0 -166
- batframework-1.0.9a7.dist-info/RECORD +0 -63
- /batframework-1.0.9a7.dist-info/LICENCE → /batframework-1.0.9a9.dist-info/LICENSE +0 -0
- {batframework-1.0.9a7.dist-info → batframework-1.0.9a9.dist-info}/top_level.txt +0 -0
batFramework/gui/meter.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import math
|
1
2
|
import batFramework as bf
|
2
3
|
from .shape import Shape
|
3
4
|
from typing import Self
|
@@ -8,7 +9,6 @@ def custom_top_at(self, x, y):
|
|
8
9
|
return self.parent
|
9
10
|
return None
|
10
11
|
|
11
|
-
|
12
12
|
class Meter(Shape):
|
13
13
|
def __init__(self, min_value: float = 0, max_value: float = 1, step: float = 0.1):
|
14
14
|
super().__init__()
|
@@ -16,14 +16,6 @@ class Meter(Shape):
|
|
16
16
|
self.step = step
|
17
17
|
self.snap: bool = False
|
18
18
|
self.value = self.max_value
|
19
|
-
self.content = Shape((0, 0)).set_color(bf.color.BLUE)
|
20
|
-
self.content.set_debug_color("cyan")
|
21
|
-
self.content.top_at = lambda x, y: custom_top_at(self.content, x, y)
|
22
|
-
self.add(self.content)
|
23
|
-
self.set_padding(4)
|
24
|
-
self.set_color("gray20")
|
25
|
-
self.set_outline_width(1)
|
26
|
-
self.set_outline_color(bf.color.BLACK)
|
27
19
|
self.set_debug_color("pink")
|
28
20
|
|
29
21
|
def __str__(self) -> str:
|
@@ -36,18 +28,11 @@ class Meter(Shape):
|
|
36
28
|
|
37
29
|
def set_range(self, range_min: float, range_max: float) -> Self:
|
38
30
|
if range_min >= range_max:
|
39
|
-
print(
|
40
|
-
f"[Warning] : minimum value {range_min} is greater than or equal to maximum value {range_max}"
|
41
|
-
)
|
42
31
|
return self
|
43
32
|
self.min_value = range_min
|
44
33
|
self.max_value = range_max
|
45
34
|
self.dirty_shape = True
|
46
35
|
|
47
|
-
def get_debug_outlines(self):
|
48
|
-
yield from super().get_debug_outlines()
|
49
|
-
# yield from self.content.get_debug_outlines()
|
50
|
-
|
51
36
|
def set_value(self, value: float) -> Self:
|
52
37
|
value = max(self.min_value, min(self.max_value, value))
|
53
38
|
value = round(value / self.step) * self.step
|
@@ -64,11 +49,48 @@ class Meter(Shape):
|
|
64
49
|
def get_ratio(self) -> float:
|
65
50
|
return (self.value-self.min_value) / (self.max_value - self.min_value)
|
66
51
|
|
52
|
+
|
53
|
+
class BarMeter(Meter):
|
54
|
+
def __init__(self, min_value: float = 0, max_value: float = 1, step: float = 0.1):
|
55
|
+
super().__init__(min_value, max_value, step)
|
56
|
+
self.axis: bf.axis = bf.axis.HORIZONTAL
|
57
|
+
self.content = Shape((0, 0)).set_color(bf.color.BLUE)
|
58
|
+
self.content.set_debug_color("cyan")
|
59
|
+
self.content.top_at = lambda x, y: custom_top_at(self.content, x, y)
|
60
|
+
self.add(self.content)
|
61
|
+
self.set_padding(4)
|
62
|
+
self.set_color("gray20")
|
63
|
+
self.set_outline_width(1)
|
64
|
+
self.set_outline_color(bf.color.BLACK)
|
65
|
+
self.set_debug_color("pink")
|
66
|
+
|
67
|
+
def __str__(self) -> str:
|
68
|
+
return "BarMeter"
|
69
|
+
|
70
|
+
def set_axis(self,axis:bf.axis)->Self:
|
71
|
+
self.axis = axis
|
72
|
+
self.dirty_shape = True
|
73
|
+
return self
|
74
|
+
|
67
75
|
def _build_content(self) -> None:
|
68
|
-
|
69
|
-
|
70
|
-
|
76
|
+
padded = self.get_inner_rect()
|
77
|
+
ratio = self.get_ratio()
|
78
|
+
|
79
|
+
self.content.set_border_radius(*[round(b/2) for b in self.border_radius])
|
80
|
+
|
81
|
+
if self.axis == bf.axis.HORIZONTAL:
|
82
|
+
width = (padded.width- self.outline_width *2) * ratio
|
83
|
+
self.content.set_size((width, padded.height - self.outline_width*2))
|
84
|
+
self.content.rect.topleft = padded.move(self.outline_width, self.outline_width).topleft
|
85
|
+
|
86
|
+
else: # vertical
|
87
|
+
height = (padded.height - self.outline_width * 2) * ratio
|
88
|
+
self.content.set_size((padded.width - self.outline_width * 2, height))
|
89
|
+
self.content.rect.bottomleft = (
|
90
|
+
padded.move(self.outline_width,-self.outline_width).bottomleft
|
91
|
+
)
|
92
|
+
self.content.rect.height = math.ceil(self.content.rect.height)
|
71
93
|
|
72
94
|
def build(self) -> None:
|
73
|
-
super().build()
|
74
95
|
self._build_content()
|
96
|
+
super().build()
|
batFramework/gui/radioButton.py
CHANGED
@@ -1,84 +1,35 @@
|
|
1
1
|
import batFramework as bf
|
2
2
|
from typing import Self, Any, Callable
|
3
3
|
from .toggle import Toggle
|
4
|
-
|
5
|
-
|
6
|
-
class RadioVariable: ...
|
4
|
+
from .syncedVar import SyncedVar
|
7
5
|
|
8
6
|
|
9
7
|
class RadioButton(Toggle):
|
10
|
-
def __init__(self, text: str = "", radio_value: Any = None) -> None:
|
8
|
+
def __init__(self, text: str = "", radio_value: Any = None, synced_var: SyncedVar = None) -> None:
|
11
9
|
super().__init__(text, None, False)
|
12
|
-
self.radio_value: Any =
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
self.radio_value: Any = radio_value if radio_value is not None else text if text else None
|
11
|
+
self.synced_var : SyncedVar = None
|
12
|
+
if synced_var:
|
13
|
+
self.link(synced_var)
|
14
|
+
|
15
|
+
def link(self,synced_var:SyncedVar)->Self:
|
16
|
+
self.synced_var = synced_var
|
17
|
+
synced_var.bind_widget(self,self._update_state)
|
18
|
+
return self
|
16
19
|
|
17
20
|
def __str__(self) -> str:
|
18
|
-
return (
|
19
|
-
f"RadioButton({self.radio_value}|{'Active' if self.value else 'Inactive'})"
|
20
|
-
)
|
21
|
+
return f"RadioButton({self.radio_value}|{'Active' if self.value else 'Inactive'})"
|
21
22
|
|
22
23
|
def set_radio_value(self, value: Any) -> Self:
|
23
24
|
self.radio_value = value
|
24
|
-
|
25
|
-
if self.radio_variable:
|
26
|
-
self.radio_variable.update_buttons()
|
27
25
|
return self
|
28
26
|
|
29
|
-
def
|
30
|
-
|
31
|
-
if
|
32
|
-
flag = True
|
33
|
-
super().set_text(text)
|
34
|
-
if flag:
|
35
|
-
self.set_radio_value(self.text)
|
36
|
-
return self
|
37
|
-
|
38
|
-
def click(self) -> None:
|
39
|
-
if self.radio_variable is None:
|
40
|
-
return
|
41
|
-
self.radio_variable.set_value(self.radio_value)
|
42
|
-
|
43
|
-
|
44
|
-
class RadioVariable:
|
45
|
-
def __init__(self) -> None:
|
46
|
-
self.buttons: list[RadioButton] = []
|
47
|
-
self.value = None
|
48
|
-
self.modify_callback: Callable[[bool],Any] = None
|
49
|
-
|
50
|
-
def set_modify_callback(self, callback:Callable[[bool],Any]) -> Self:
|
51
|
-
self.modify_callback = callback
|
52
|
-
return self
|
53
|
-
|
54
|
-
def link(self, *buttons: RadioButton) -> Self:
|
55
|
-
if not buttons:
|
56
|
-
return self
|
57
|
-
for b in buttons:
|
58
|
-
b.radio_variable = self
|
59
|
-
self.buttons.extend(buttons)
|
60
|
-
|
61
|
-
if self.value is None:
|
62
|
-
self.set_value(buttons[0].radio_value)
|
63
|
-
else:
|
64
|
-
self.update_buttons()
|
65
|
-
return self
|
66
|
-
|
67
|
-
def unlink(self, *buttons) -> Self:
|
68
|
-
for b in self.buttons:
|
69
|
-
if b in buttons:
|
70
|
-
b.radio_variable = None
|
71
|
-
self.buttons = [b for b in self.buttons if b not in buttons]
|
72
|
-
return self
|
73
|
-
|
74
|
-
def set_value(self, value: Any) -> Self:
|
75
|
-
if value == self.value:
|
76
|
-
return
|
77
|
-
self.value = value
|
78
|
-
if self.modify_callback:
|
79
|
-
self.modify_callback(value)
|
80
|
-
self.update_buttons()
|
81
|
-
return self
|
27
|
+
def set_value(self, value : bool, do_callback=False):
|
28
|
+
super().set_value(value, do_callback)
|
29
|
+
if value : self.synced_var.value = self.radio_value
|
82
30
|
|
83
|
-
def
|
84
|
-
|
31
|
+
def _update_state(self, synced_value: Any) -> None:
|
32
|
+
"""
|
33
|
+
Updates the state of the RadioButton based on the synced variable's value.
|
34
|
+
"""
|
35
|
+
self.set_value(self.radio_value == synced_value, False)
|
batFramework/gui/root.py
CHANGED
@@ -15,15 +15,28 @@ class Root(InteractiveWidget):
|
|
15
15
|
self.rect.size = pygame.display.get_surface().get_size()
|
16
16
|
self.focused: InteractiveWidget | None = self
|
17
17
|
self.hovered: Widget | None = self
|
18
|
-
self.set_debug_color("yellow")
|
19
|
-
self.set_render_order(sys.maxsize)
|
20
18
|
self.clip_children = False
|
19
|
+
self.set_debug_color("yellow")
|
21
20
|
|
21
|
+
self.show_tooltip : bool = True
|
22
|
+
self.tooltip = bf.gui.ToolTip("").set_visible(False)
|
23
|
+
self.add(self.tooltip)
|
24
|
+
|
25
|
+
def toggle_tooltip(self,value:bool)->Self:
|
26
|
+
self.show_tooltip = value
|
27
|
+
return self
|
28
|
+
|
22
29
|
def __str__(self) -> str:
|
23
30
|
return "Root"
|
31
|
+
|
32
|
+
def to_ascii_tree(self) -> str:
|
33
|
+
def f(w, depth):
|
34
|
+
prefix = " " * (depth * 4) + ("└── " if depth > 0 else "")
|
35
|
+
children = "\n".join(f(c, depth + 1) for c in w.children) if w.children else ""
|
36
|
+
return f"{prefix}{str(w)}\n{children}"
|
37
|
+
return f(self, 0)
|
24
38
|
|
25
39
|
def set_parent_scene(self, parent_scene: bf.Scene) -> Self:
|
26
|
-
bf.StyleManager().register_widget(self)
|
27
40
|
return super().set_parent_scene(parent_scene)
|
28
41
|
|
29
42
|
def get_focused(self) -> Widget | None:
|
@@ -91,28 +104,35 @@ class Root(InteractiveWidget):
|
|
91
104
|
if not force:
|
92
105
|
return self
|
93
106
|
self.rect.size = size
|
107
|
+
self.dirty_shape = True
|
108
|
+
self.dirty_size_constraints = True
|
94
109
|
return self
|
95
110
|
|
96
|
-
def
|
111
|
+
def process_event(self,event):
|
112
|
+
if not event.consumed : self.do_handle_event_early(event)
|
113
|
+
if not event.consumed : super().process_event(event)
|
114
|
+
|
115
|
+
def do_handle_event_early(self, event):
|
116
|
+
if event.type == pygame.VIDEORESIZE and not pygame.SCALED & bf.const.FLAGS:
|
117
|
+
self.set_size((event.w,event.h),force=True)
|
97
118
|
if self.focused:
|
98
119
|
if event.type == pygame.KEYDOWN:
|
99
|
-
|
100
|
-
|
120
|
+
event.consumed = self.focused.on_key_down(event.key)
|
121
|
+
if not event.consumed :
|
122
|
+
event.consumed = self._handle_alt_tab(event.key)
|
101
123
|
elif event.type == pygame.KEYUP:
|
102
|
-
|
103
|
-
event.consumed = True
|
124
|
+
event.consumed = self.focused.on_key_up(event.key)
|
104
125
|
|
105
126
|
if not self.hovered or (not isinstance(self.hovered, InteractiveWidget)):
|
127
|
+
event.consumed = True
|
106
128
|
return
|
107
129
|
|
108
130
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
109
|
-
|
110
|
-
event.consumed = True
|
131
|
+
event.consumed = self.hovered.on_click_down(event.button)
|
111
132
|
|
112
133
|
elif event.type == pygame.MOUSEBUTTONUP:
|
113
|
-
|
114
|
-
|
115
|
-
|
134
|
+
event.consumed = self.hovered.on_click_up(event.button)
|
135
|
+
|
116
136
|
def do_on_click_down(self, button: int) -> None:
|
117
137
|
if button == 1:
|
118
138
|
self.clear_focused()
|
@@ -127,25 +147,69 @@ class Root(InteractiveWidget):
|
|
127
147
|
|
128
148
|
def update(self, dt: float) -> None:
|
129
149
|
super().update(dt)
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
150
|
+
self.update_tree()
|
151
|
+
|
152
|
+
mouse_screen = pygame.mouse.get_pos()
|
153
|
+
mouse_world = self.drawing_camera.screen_to_world(mouse_screen)
|
154
|
+
prev_hovered = self.hovered
|
155
|
+
self.hovered = self.top_at(*mouse_world) or None
|
156
|
+
|
157
|
+
# Tooltip logic
|
158
|
+
if self.hovered and self.hovered.tooltip_text:
|
159
|
+
self.tooltip.set_text(self.hovered.tooltip_text)
|
160
|
+
|
161
|
+
tooltip_size = self.tooltip.get_min_required_size()
|
162
|
+
screen_w, screen_h = self.drawing_camera.rect.size
|
163
|
+
tooltip_x, tooltip_y = self.drawing_camera.world_to_screen_point(mouse_world)
|
164
|
+
|
165
|
+
tooltip_x = min(tooltip_x, screen_w - tooltip_size[0])
|
166
|
+
tooltip_y = min(tooltip_y, screen_h - tooltip_size[1])
|
167
|
+
tooltip_x = max(0, tooltip_x)
|
168
|
+
tooltip_y = max(0, tooltip_y)
|
169
|
+
|
170
|
+
self.tooltip.set_position(tooltip_x, tooltip_y)
|
171
|
+
self.tooltip.fade_in()
|
172
|
+
else:
|
173
|
+
self.tooltip.fade_out()
|
174
|
+
|
175
|
+
if self.hovered == prev_hovered:
|
176
|
+
if isinstance(self.hovered, InteractiveWidget):
|
177
|
+
self.hovered.on_mouse_motion(*mouse_world)
|
135
178
|
return
|
136
|
-
|
137
|
-
|
138
|
-
|
179
|
+
|
180
|
+
if isinstance(prev_hovered, InteractiveWidget):
|
181
|
+
prev_hovered.on_exit()
|
182
|
+
if isinstance(self.hovered, InteractiveWidget):
|
139
183
|
self.hovered.on_enter()
|
140
184
|
|
141
|
-
def apply_updates(self):
|
142
|
-
if any(child.dirty_shape for child in self.children):
|
143
|
-
self.dirty_shape = True # Mark layout as dirty if any child changed size
|
144
185
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
186
|
+
def _handle_alt_tab(self,key):
|
187
|
+
if self.focused is None:
|
188
|
+
return False
|
189
|
+
if key != pygame.K_TAB:
|
190
|
+
return False
|
191
|
+
keys = pygame.key.get_pressed()
|
192
|
+
if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
|
193
|
+
self.focused.focus_prev_tab(self.focused)
|
194
|
+
else:
|
195
|
+
self.focused.focus_next_tab(self.focused)
|
196
|
+
return True
|
197
|
+
|
198
|
+
def update_tree(self):
|
199
|
+
# print("START updating tree")
|
200
|
+
self.apply_updates("pre")
|
201
|
+
self.apply_updates("post")
|
202
|
+
|
203
|
+
self.apply_updates("pre")
|
204
|
+
self.apply_updates("post")
|
205
|
+
|
206
|
+
# print("END updating tree")
|
207
|
+
|
208
|
+
def apply_pre_updates(self):
|
209
|
+
return
|
210
|
+
|
211
|
+
def apply_post_updates(self, skip_draw = False):
|
212
|
+
return
|
149
213
|
|
150
214
|
def draw(self, camera: bf.Camera) -> None:
|
151
215
|
super().draw(camera)
|
@@ -155,4 +219,10 @@ class Root(InteractiveWidget):
|
|
155
219
|
and self.focused
|
156
220
|
and self.focused != self
|
157
221
|
):
|
158
|
-
self.focused.
|
222
|
+
clip:bool =self.focused.parent and self.focused.parent.clip_children
|
223
|
+
if clip:
|
224
|
+
old_clip = camera.surface.get_clip()
|
225
|
+
camera.surface.set_clip(camera.world_to_screen(self.focused.parent.rect))
|
226
|
+
if clip:
|
227
|
+
camera.surface.set_clip(old_clip)
|
228
|
+
self.focused.draw_focused(camera)
|
@@ -0,0 +1,250 @@
|
|
1
|
+
import batFramework as bf
|
2
|
+
import pygame
|
3
|
+
from typing import Self,Callable,Any
|
4
|
+
from .button import Button
|
5
|
+
from .indicator import ArrowIndicator
|
6
|
+
from .clickableWidget import ClickableWidget
|
7
|
+
from .widget import Widget
|
8
|
+
|
9
|
+
class MyArrow(ArrowIndicator,ClickableWidget):
|
10
|
+
def top_at(self,x,y):
|
11
|
+
return Widget.top_at(self,x,y)
|
12
|
+
|
13
|
+
def get_focus(self):
|
14
|
+
return self.parent.get_focus()
|
15
|
+
|
16
|
+
class Selector(Button):
|
17
|
+
def __init__(self,options:list[str]=None,default_value:str=None,allow_cycle:bool=False):
|
18
|
+
super().__init__('')
|
19
|
+
self.allow_cycle = allow_cycle
|
20
|
+
self.current_index = 0
|
21
|
+
self.on_modify_callback : Callable[[str,int],Any] = None
|
22
|
+
self.options = options if options else []
|
23
|
+
self.gap : int = 2
|
24
|
+
text_value = ""
|
25
|
+
|
26
|
+
|
27
|
+
if not (default_value is not None and default_value in self.options):
|
28
|
+
default_value = options[0]
|
29
|
+
|
30
|
+
text_value = default_value
|
31
|
+
self.current_index = self.options.index(default_value)
|
32
|
+
|
33
|
+
self.left_indicator : MyArrow = (MyArrow(bf.direction.LEFT)
|
34
|
+
.set_color((0,0,0,0)).set_arrow_color(self.text_color)
|
35
|
+
.set_callback(lambda : self.set_by_index(self.get_current_index()-1))
|
36
|
+
)
|
37
|
+
|
38
|
+
self.right_indicator:MyArrow =(MyArrow(bf.direction.RIGHT)
|
39
|
+
.set_color((0,0,0,0)).set_arrow_color(self.text_color)
|
40
|
+
.set_callback(lambda : self.set_by_index(self.get_current_index()+1))
|
41
|
+
)
|
42
|
+
|
43
|
+
self.add(self.left_indicator,self.right_indicator)
|
44
|
+
self.set_clip_children(False)
|
45
|
+
self.set_text(text_value)
|
46
|
+
|
47
|
+
def __str__(self):
|
48
|
+
return f"Selector[{self.options[self.current_index]}]"
|
49
|
+
|
50
|
+
def set_gap(self,value:int)->Self:
|
51
|
+
self.gap = value
|
52
|
+
self.dirty_shape = True
|
53
|
+
return self
|
54
|
+
|
55
|
+
def set_arrow_color(self,color)->Self:
|
56
|
+
self.left_indicator.set_arrow_color(color)
|
57
|
+
self.right_indicator.set_arrow_color(color)
|
58
|
+
return self
|
59
|
+
|
60
|
+
def disable(self):
|
61
|
+
super().disable()
|
62
|
+
self.left_indicator.disable()
|
63
|
+
self.right_indicator.disable()
|
64
|
+
return self
|
65
|
+
|
66
|
+
def enable(self):
|
67
|
+
super().enable()
|
68
|
+
self.right_indicator.enable()
|
69
|
+
self.left_indicator.enable()
|
70
|
+
index = self.current_index
|
71
|
+
if not self.allow_cycle:
|
72
|
+
if index == 0:
|
73
|
+
self.left_indicator.disable()
|
74
|
+
else:
|
75
|
+
self.left_indicator.enable()
|
76
|
+
if index == len(self.options)-1:
|
77
|
+
self.right_indicator.disable()
|
78
|
+
else:
|
79
|
+
self.right_indicator.enable()
|
80
|
+
|
81
|
+
|
82
|
+
return self
|
83
|
+
|
84
|
+
def set_tooltip_text(self, text):
|
85
|
+
self.left_indicator.set_tooltip_text(text)
|
86
|
+
self.right_indicator.set_tooltip_text(text)
|
87
|
+
return super().set_tooltip_text(text)
|
88
|
+
|
89
|
+
def get_min_required_size(self) -> tuple[float, float]:
|
90
|
+
"""
|
91
|
+
Calculates the minimum size required for the selector, including text and indicators.
|
92
|
+
"""
|
93
|
+
|
94
|
+
# Calculate the maximum size needed for the text
|
95
|
+
old_text = self.text
|
96
|
+
max_text_size = (0, 0)
|
97
|
+
for option in self.options:
|
98
|
+
self.text = option
|
99
|
+
text_size = self._get_text_rect_required_size()
|
100
|
+
max_text_size = max(max_text_size[0], text_size[0]), max(max_text_size[1], text_size[1])
|
101
|
+
self.text = old_text
|
102
|
+
|
103
|
+
# Ensure total_height is always an odd integer
|
104
|
+
total_height = max(self.font_object.get_height()+1, max_text_size[1] * 1.5)
|
105
|
+
total_height += self.unpressed_relief
|
106
|
+
total_height += max(self.right_indicator.outline_width,self.left_indicator.outline_width)
|
107
|
+
|
108
|
+
# Calculate total width and height
|
109
|
+
total_width = (
|
110
|
+
total_height*2+
|
111
|
+
max_text_size[0] + # Text width
|
112
|
+
self.gap * 2 # Gaps between text and indicators
|
113
|
+
)
|
114
|
+
# Inflate by padding at the very end
|
115
|
+
final_size = self.expand_rect_with_padding((0, 0, total_width, total_height)).size
|
116
|
+
|
117
|
+
return final_size
|
118
|
+
|
119
|
+
def _align_content(self):
|
120
|
+
"""
|
121
|
+
Builds the selector layout (places and resizes the indicators)
|
122
|
+
"""
|
123
|
+
|
124
|
+
# return
|
125
|
+
# Step 1: Calculate the padded area for positioning
|
126
|
+
padded = self.get_inner_rect()
|
127
|
+
|
128
|
+
|
129
|
+
# left_size = self.left_indicator.rect.size
|
130
|
+
right_size = self.right_indicator.rect.size
|
131
|
+
|
132
|
+
indicator_height = padded.h
|
133
|
+
|
134
|
+
self.left_indicator.set_size((indicator_height,indicator_height))
|
135
|
+
self.right_indicator.set_size((indicator_height,indicator_height))
|
136
|
+
|
137
|
+
|
138
|
+
# Step 3: Position indicators
|
139
|
+
self.left_indicator.set_position(padded.left, None)
|
140
|
+
self.left_indicator.set_center(None, padded.centery)
|
141
|
+
|
142
|
+
self.right_indicator.set_position(padded.right - right_size[0], None)
|
143
|
+
self.right_indicator.set_center(None, padded.centery)
|
144
|
+
|
145
|
+
def apply_post_updates(self, skip_draw = False):
|
146
|
+
super().apply_post_updates(skip_draw)
|
147
|
+
self._align_content()
|
148
|
+
|
149
|
+
def get_current_index(self)->int:
|
150
|
+
return self.current_index
|
151
|
+
|
152
|
+
def set_allow_cycle(self,value:bool)->Self:
|
153
|
+
if value == self.allow_cycle: return self
|
154
|
+
self.allow_cycle = value
|
155
|
+
self.dirty_surface = True
|
156
|
+
return self
|
157
|
+
|
158
|
+
def set_text_color(self,color)->Self:
|
159
|
+
super().set_text_color(color)
|
160
|
+
self.left_indicator.set_arrow_color(color)
|
161
|
+
self.right_indicator.set_arrow_color(color)
|
162
|
+
return self
|
163
|
+
|
164
|
+
def set_modify_callback(self,function:Callable[[str,int],Any]):
|
165
|
+
"""
|
166
|
+
the function will receive the value and the index
|
167
|
+
"""
|
168
|
+
self.on_modify_callback = function
|
169
|
+
return self
|
170
|
+
|
171
|
+
def set_by_index(self,index:int)->Self:
|
172
|
+
if self.allow_cycle:
|
173
|
+
index = index%len(self.options)
|
174
|
+
else:
|
175
|
+
index = max(min(len(self.options)-1,index),0)
|
176
|
+
|
177
|
+
if index == self.current_index:
|
178
|
+
return self
|
179
|
+
|
180
|
+
self.current_index = index
|
181
|
+
self.set_text(self.options[self.current_index])
|
182
|
+
if self.on_modify_callback:
|
183
|
+
self.on_modify_callback(self.options[self.current_index],self.current_index)
|
184
|
+
if not self.allow_cycle:
|
185
|
+
if index == 0:
|
186
|
+
self.left_indicator.disable()
|
187
|
+
else:
|
188
|
+
self.left_indicator.enable()
|
189
|
+
if index == len(self.options)-1:
|
190
|
+
self.right_indicator.disable()
|
191
|
+
else:
|
192
|
+
self.right_indicator.enable()
|
193
|
+
return self
|
194
|
+
|
195
|
+
def paint(self):
|
196
|
+
super().paint()
|
197
|
+
self.left_indicator.show()
|
198
|
+
self.right_indicator.show()
|
199
|
+
if not self.allow_cycle and self.current_index == 0:
|
200
|
+
self.left_indicator.hide()
|
201
|
+
elif not self.allow_cycle and self.current_index== len(self.options)-1:
|
202
|
+
self.right_indicator.hide()
|
203
|
+
|
204
|
+
def set_by_value(self,value:str)->Self:
|
205
|
+
if not self.is_enabled : return
|
206
|
+
if value not in self.options : return self
|
207
|
+
index = self.options.index(value)
|
208
|
+
self.set_by_index(index)
|
209
|
+
return self
|
210
|
+
|
211
|
+
def on_key_down(self, key: int) -> bool:
|
212
|
+
if not self.is_enabled:
|
213
|
+
return False
|
214
|
+
|
215
|
+
key_actions = {
|
216
|
+
pygame.K_RIGHT: self.right_indicator,
|
217
|
+
pygame.K_SPACE: self.right_indicator,
|
218
|
+
pygame.K_LEFT: self.left_indicator
|
219
|
+
}
|
220
|
+
|
221
|
+
indicator = key_actions.get(key)
|
222
|
+
if indicator and indicator.visible and indicator.is_enabled:
|
223
|
+
indicator.on_click_down(1)
|
224
|
+
return True
|
225
|
+
|
226
|
+
return False
|
227
|
+
|
228
|
+
def on_key_up(self, key: int) -> bool:
|
229
|
+
if not self.is_enabled:
|
230
|
+
return False
|
231
|
+
|
232
|
+
key_actions = {
|
233
|
+
pygame.K_RIGHT: self.right_indicator,
|
234
|
+
pygame.K_SPACE: self.right_indicator,
|
235
|
+
pygame.K_LEFT: self.left_indicator
|
236
|
+
}
|
237
|
+
|
238
|
+
indicator = key_actions.get(key)
|
239
|
+
if indicator and indicator.visible and indicator.is_enabled:
|
240
|
+
indicator.on_click_up(1)
|
241
|
+
return True
|
242
|
+
|
243
|
+
return False
|
244
|
+
|
245
|
+
def do_on_click_down(self, button) -> None:
|
246
|
+
if self.is_enabled and button == 1:
|
247
|
+
if not self.get_focus():
|
248
|
+
return True
|
249
|
+
return False
|
250
|
+
|
batFramework/gui/shape.py
CHANGED
@@ -18,16 +18,24 @@ class Shape(Widget):
|
|
18
18
|
self.shadow_color: tuple[int, int, int] | str = (0, 0, 0, 255)
|
19
19
|
self.draw_mode = bf.drawMode.SOLID
|
20
20
|
|
21
|
-
def
|
21
|
+
def get_inner_bottom(self) -> float:
|
22
22
|
return self.rect.bottom - self.padding[3] - self.relief
|
23
23
|
|
24
|
-
def
|
24
|
+
def get_inner_height(self) -> float:
|
25
25
|
return self.rect.h - self.padding[1] - self.padding[3] - self.relief
|
26
26
|
|
27
|
-
def
|
28
|
-
return self.rect.y + self.
|
27
|
+
def get_inner_top(self) -> float:
|
28
|
+
return self.rect.y + self.padding[1]
|
29
29
|
|
30
|
-
def
|
30
|
+
def get_local_inner_rect(self)->pygame.FRect:
|
31
|
+
return pygame.FRect(
|
32
|
+
self.padding[0],
|
33
|
+
self.padding[1],
|
34
|
+
self.rect.w - self.padding[2] - self.padding[0],
|
35
|
+
self.rect.h - self.padding[1] - self.padding[3] - self.relief,
|
36
|
+
)
|
37
|
+
|
38
|
+
def get_inner_rect(self) -> pygame.FRect:
|
31
39
|
return pygame.FRect(
|
32
40
|
self.rect.x + self.padding[0],
|
33
41
|
self.rect.y + self.padding[1],
|