8000 gui: rename Focusable, add documentation about controller support · pythonarcade/arcade@f8bb36b · GitHub
[go: up one dir, main page]

Skip to content

Commit f8bb36b

Browse files
committed
gui: rename Focusable, add documentation about controller support
1 parent b9bc901 commit f8bb36b

File tree

5 files changed

+216
-20
lines changed

5 files changed

+216
-20
lines changed

arcade/examples/gui/exp_controller_inventory.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
UIEvent,
4545
)
4646
from arcade.gui.events import UIControllerButtonPressEvent
47-
from arcade.gui.experimental.focus import Focusable, UIFocusGroup
47+
from arcade.gui.experimental.focus import UIFocusable, UIFocusGroup
4848
from arcade.resources import load_kenney_fonts
4949

5050

@@ -143,7 +143,7 @@ def legs(self, value):
143143
self[2] = value
144144

145145

146-
class InventorySlotUI(Focusable, UIFlatButton):
146+
class InventorySlotUI(UIFocusable, UIFlatButton):
147147
"""Represents a single inventory slot.
148148
The slot accesses a specific index in the inventory.
149149

arcade/examples/gui/exp_controller_support_grid.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
UIView,
2121
UIWidget,
2222
)
23-
from arcade.gui.experimental.focus import Focusable, UIFocusGroup
23+
from arcade.gui.experimental.focus import UIFocusable, UIFocusGroup
2424

2525

26-
class FocusableButton(Focusable, UIFlatButton):
26+
class FocusableButton(UIFocusable, UIFlatButton):
2727
pass
2828

2929

@@ -43,7 +43,7 @@ def setup_grid_focus_transition(grid: Dict[Tuple[int, int], UIWidget]):
4343
for c in range(cols):
4444
for r in range(rows):
4545
btn = grid.get((c, r))
46-
if btn is None or not isinstance(btn, Focusable):
46+
if btn is None or not isinstance(btn, UIFocusable):
4747
continue
4848

4949
if c > 0:

arcade/gui/experimental/focus.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from arcade.gui.widgets.slider import UIBaseSlider
2626

2727

28-
class Focusable(UIWidget):
28+
class UIFocusable(UIWidget):
2929
"""
3030
A widget that provides additional information about focus neighbors.
3131
@@ -98,12 +98,12 @@ def on_event(self, event: UIEvent) -> Optional[bool]:
9898
return EVENT_HANDLED
9999

100100
elif event.symbol == arcade.key.SPACE:
101-
self.start_interaction()
101+
self._start_interaction()
102102
return EVENT_HANDLED
103103

104104
elif isinstance(event, UIKeyReleaseEvent):
105105
if event.symbol == arcade.key.SPACE:
106-
self.end_interaction()
106+
self._end_interaction()
107107
return EVENT_HANDLED
108108

109109
elif isinstance(event, UIControllerDpadEvent):
@@ -140,11 +140,11 @@ def on_event(self, event: UIEvent) -> Optional[bool]:
140140

141141
elif isinstance(event, UIControllerButtonPressEvent):
142142
if event.button == "a":
143-
self.start_interaction()
143+
self._start_interaction()
144144
return EVENT_HANDLED
145145
elif isinstance(event, UIControllerButtonReleaseEvent):
146146
if event.button == "a":
147-
self.end_interaction()
147+
self._end_interaction()
148148
return EVENT_HANDLED
149149

150150
return EVENT_UNHANDLED
@@ -213,7 +213,7 @@ def set_focus(self, widget: UIWidget | None | EllipsisType = ...):
213213

214214
def focus_up(self):
215215
widget = self.focused_widget
216-
if isinstance(widget, Focusable):
216+
if isinstance(widget, UIFocusable):
217217
if widget.neighbor_up:
218218
self.set_focus(widget.neighbor_up)
219219
return
@@ -222,7 +222,7 @@ def focus_up(self):
222222

223223
def focus_down(self):
224224
widget = self.focused_widget
225-
if isinstance(widget, Focusable):
225+
if isinstance(widget, UIFocusable):
226226
if widget.neighbor_down:
227227
self.set_focus(widget.neighbor_down)
228228
return
@@ -231,7 +231,7 @@ def focus_down(self):
231231

232232
def focus_left(self):
233233
widget = self.focused_widget
234-
if isinstance(widget, Focusable):
234+
if isinstance(widget, UIFocusable):
235235
if widget.neighbor_left:
236236
self.set_focus(widget.neighbor_left)
237237
return
@@ -240,7 +240,7 @@ def focus_left(self):
240240

241241
def focus_right(self):
242242
widget = self.focused_widget
243-
if isinstance(widget, Focusable):
243+
if isinstance(widget, UIFocusable):
244244
if widget.neighbor_right:
245245
self.set_focus(widget.neighbor_right)
246246
return
@@ -275,7 +275,7 @@ def focus_previous(self):
275275
# automatically wrap around via index -1
276276
self.set_focus(self._focusable_widgets[focused_index])
277277

278-
def start_interaction(self):
278+
def _start_interaction(self):
279279
# TODO this should be handled in the widget
280280

281281
widget = self.focused_widget
@@ -294,7 +294,7 @@ def start_interaction(self):
294294
else:
295295
print("Cannot interact widget")
296296

297-
def end_interaction(self):
297+
def _end_interaction(self):
298298
widget = self.focused_widget
299299

300300
if isinstance(widget, UIInteractiveWidget):
@@ -334,7 +334,7 @@ def do_post_render(self, surface: Surface):
334334

335335
if self._debug:
336336
# debugging
337-
if isinstance(widget, Focusable):
337+
if isinstance(widget, UIFocusable):
338338
if widget.neighbor_up:
339339
self._draw_indicator(
340340
widget.rect.top_center,
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
.. _gui_controller_support:
2+
3+
GUI Controller Support
4+
----------------------
5+
6+
The `arcade.gui` module now includes **experimental controller support**, allowing you to navigate through GUI elements using a game controller. This feature introduces the `ControllerWindow` and `ControllerView` classes, which provide controller-specific functionality.
7+
8+
Below is a guide on how to set up and use this feature effectively.
9+
10+
Basic Setup
11+
~~~~~~~~~~~
12+
13+
To use controller support, you need to use the `ControllerWindow` and `ControllerView` classes.
14+
These classes provide the necessary hooks for handling controller input and managing focus within the GUI.
15+
16+
The following code makes use of the `UIView` class, which simplifies the process of setting up a view with a `UIManager`.
17+
18+
Setting Up Controller Support
19+
`````````````````````````````
20+
21+
The `ControllerWindow` is an instance of `arcade.Window` that integrates controller input handling. The `ControllerView` class provides controller-specific callbacks,
22+
which are used by the `UIManager` to handle controller events.
23+
24+
Below is an example of how to set up a controller-enabled application:
25+
26+
.. code-block:: python
27+
28+
import arcade
29+
from arcade.gui import UIView
30+
from arcade.experimental.controller_window import ControllerWindow, ControllerView
31+
32+
33+
class MyControllerView(ControllerView, UIView):
34+
def __init__(self):
35+
super().__init__()
36+
37+
# Initialize your GUI elements here
38+
39+
# react to controller events for your game
40+
def on_connect(self, controller):
41+
print(f"Controller connected: {controller}")
42+
43+
def on_disconnect(self, controller):
44+
print(f"Controller disconnected: {controller}")
45+
46+
def on_stick_motion(self, controller, stick, value):
47+
print(f"Stick {stick} moved to {value} on controller {controller}")
48+
49+
def on_trigger_motion(self, controller, trigger, value):
50+
print(f"Trigger {trigger} moved to {value} on controller {controller}")
51+
52+
def on_button_press(self, controller, button):
53+
print(f"Button {button} pressed on controller {controller}")
54+
55+
def on_button_release(self, controller, button):
56+
print(f"Button {button} released on controller {controller}")
57+
58+
def on_dpad_motion(self, controller, value):
59+
print(f"D-Pad moved to {value} on controller {controller}")
60+
61+
62+
if __name__ == "__main__":
63+
window = ControllerWindow(title="Controller Support Example")
64+
view = MyControllerView()
65+
window.show_view(view)
66+
arcade.run()
67+
68+
69+
Managing Focus with `FocusGroups`
70+
`````````````````````````````````
71+
72+
To enable controller navigation, you must group your interactive GUI elements into a `UIFocusGroup`.
73+
A `UIFocusGroup` allows the controller to cycle through the elements and ensures that only one element is focused at a time.
74+
75+
A single `UIFocusGroup` can be added to the `UIManager` as a root widget acting as a `UIAnchorLayout`.
76+
77+
.. code-block:: python
78+
79+
from arcade.experimental.controller_window import ControllerView, ControllerWindow
80+
from arcade.gui import UIFlatButton, UIBoxLayout, UIView
81+
from arcade.gui.experimental.focus import UIFocusGroup
82+
83+
84+
class MyControllerView(ControllerView, UIView):
85+
def __init__(self):
86+
super().__init__()
87+
88+
# Create buttons and add them to the focus group
89+
fg = UIFocusGroup()
90+
self.ui.add(fg)
91+
92+
box = UIBoxLayout()
93+
fg.add(box)
94+
95+
button1 = UIFlatButton(text="Button 1", width=200)
96+
button2 = UIFlatButton(text="Button 2", width=200)
97+
98+
box.add(button1)
99+
box.add(button2)
100+
101+
# initialize the focus group, detect focusable widgets and set the initial focus
102+
fg.detect_focusable_widgets()
103+
fg.set_focus()
104+
105+
106+
if __name__ == "__main__":
107+
window = ControllerWindow(title="Controller Support Example")
108+
view = MyControllerView()
109+
window.show_view(view)
110+
window.run()
111+
112+
113+
Setting Initial Focus
114+
`````````````````````
115+
116+
It is essential to set the initial focus for the `UIFocusGroup`. Without this, the controller will not know which element to start with.
117+
118+
.. code-block:: python
119+
120+
# Set the initial focus
121+
self.focus_group.set_focus()
122+
123+
Summary
124+
```````
125+
To use the experimental controller support in `arcade.gui`:
126+
127+
1. Use `ControllerWindow` as your main application window.
128+
2. Use `ControllerView` to provide controller-specific callbacks for the `UIManager`.
129+
3. Group interactive elements into a `UIFocusGroup` for navigation.
130+
4. Set the initial focus for the `UIFocusGroup` to enable proper navigation.
131+
132+
This setup allows you to create a fully functional GUI that can be navigated using a game controller. Note that this feature is experimental and may be subject to changes in future releases.
133+
134+
135+
Advanced Usage
136+
~~~~~~~~~~~~~~
137+
138+
Nested `UIFocusGroups`
139+
``````````````````````
140+
141+
When using nested `UIFocusGroups`, only one `UIFocusGroup` will be navigated at a time.
142+
This is particularly useful for scenarios like modals or overlays, where you want to temporarily restrict navigation to
143+
a specific set of elements. For example, the `UIDropdown` widget uses this feature to handle focus within its dropdown
144+
menu while isolating it from the rest of the interface.
145+
146+
147+
Advanced focus direction
148+
````````````````````````
149+
150+
To provide a more advanced focus direction, you can use the `UIFocusable` class.
151+
152+
The `UIFocusable` class allows you to define directional neighbors (`neighbor_up`, `neighbor_down`, `neighbor_left`, `neighbor_right`) for a widget.
153+
These neighbors determine how focus moves between widgets when navigating with a controller or keyboard.
154+
155+
Here is an example of how to use the `UIFocusable` class:
156+
157+
.. code-block:: python
158+
159+
from arcade.gui import UIFlatButton, UIGridLayout
160+
from arcade.gui.experimental.focus import UIFocusGroup, UIFocusable
161+
162+
class MyButton(UIFlatButton, UIFocusable):
163+
def __init__(self, text, width):
164+
super().__init__(text=text, width=width)
165+
166+
167+
# Create focusable buttons
168+
button1 = MyButton(text="Button 1", width=200)
169+
button2 = MyButton(text="Button 2", width=200)
170+
button3 = MyButton(text="Button 3", width=200)
171+
button4 = MyButton(text="Button 4", width=200)
172+
173+
# Set directional neighbors
174+
button1.neighbor_right = button2
175+
button1.neighbor_down = button3
176+
button2.neighbor_left = button1
177+
button2.neighbor_down = button4
178+
button3.neighbor_up = button1
179+
button3.neighbor_right = button4
180+
button4.neighbor_up = button2
181+
button4.neighbor_left = button3
182+
183+
# Add buttons to a focus group
184+
fg = UIFocusGroup()
185+
186+
grid_layout = UIGridLayout(column_count=2, row_count=2, vertical_spacing=10)
187+
grid_layout.add(button1, col_num=0, row_num=0)
188+
grid_layout.add(button2, col_num=1, row_num=0)
189+
grid_layout.add(button3, col_num=0, row_num=1)
190+
grid_layout.add(button4, col_num=1, row_num=1)
191+
192+
fg.add(grid_layout)
193+
194+
# Detect focusable widgets and set the initial focus
195+
fg.detect_focusable_widgets()
196+
fg.set_focus(button1)
197+
198+
This setup allows you to define custom navigation paths between widgets, enabling more complex focus behavior.

doc/programming_guide/gui/index.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,4 @@ Find the required information in the following sections:
3535
style
3636
own_widgets
3737
own_layout
38-
39-
40-
38+
controller_support

0 commit comments

Comments
 (0)
0