|
| 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. |
0 commit comments