gamepad: Add initial controller navigation (#406)
Co-authored-by: kramo <git@kramo.page> Co-authored-by: Zoey Ahmed <zoethetransrat@gmail.com> Reviewed-on: https://codeberg.org/kramo/cartridges/pulls/406 Reviewed-by: Laura Kramolis <git@kramo.page> Reviewed-by: Jamie Gravendeel <me@jamie.garden> Co-authored-by: Zoey Ahmed <zoeyahmed10@proton.me> Co-committed-by: Zoey Ahmed <zoeyahmed10@proton.me>
This commit is contained in:
committed by
Laura Kramolis
parent
6b735c8cf6
commit
eef38f73f5
@@ -15,6 +15,10 @@ gi.require_versions({
|
|||||||
"Adw": "1",
|
"Adw": "1",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
gi.require_version("Manette", "0.2")
|
||||||
|
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
from .config import APP_ID, LOCALEDIR, PKGDATADIR
|
from .config import APP_ID, LOCALEDIR, PKGDATADIR
|
||||||
|
|||||||
342
cartridges/gamepads.py
Normal file
342
cartridges/gamepads.py
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed
|
||||||
|
|
||||||
|
import math
|
||||||
|
from collections.abc import Generator
|
||||||
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
|
from gi.repository import Adw, Gio, GLib, GObject, Gtk, Manette
|
||||||
|
|
||||||
|
from .ui.game_item import GameItem
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .ui.window import Window
|
||||||
|
|
||||||
|
STICK_DEADZONE = 0.5
|
||||||
|
REPEAT_DELAY = 280
|
||||||
|
|
||||||
|
|
||||||
|
class Gamepad(GObject.Object):
|
||||||
|
"""Data class for gamepad, including UI navigation."""
|
||||||
|
|
||||||
|
window: "Window"
|
||||||
|
device: Manette.Device
|
||||||
|
|
||||||
|
def __init__(self, device: Manette.Device, **kwargs: Any):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.device = device
|
||||||
|
self._allowed_inputs = {
|
||||||
|
Gtk.DirectionType.UP,
|
||||||
|
Gtk.DirectionType.DOWN,
|
||||||
|
Gtk.DirectionType.LEFT,
|
||||||
|
Gtk.DirectionType.RIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.device.connect("button-press-event", self._on_button_press_event)
|
||||||
|
self.device.connect("absolute-axis-event", self._on_analog_axis_event)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_rtl_direction(
|
||||||
|
a: Gtk.DirectionType, b: Gtk.DirectionType
|
||||||
|
) -> Gtk.DirectionType:
|
||||||
|
return a if Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL else b
|
||||||
|
|
||||||
|
def _lock_input(self, direction: Gtk.DirectionType):
|
||||||
|
self._allowed_inputs.remove(direction)
|
||||||
|
GLib.timeout_add(REPEAT_DELAY, lambda *_: self._allowed_inputs.add(direction))
|
||||||
|
|
||||||
|
def _on_button_press_event(self, _device: Manette.Device, event: Manette.Event):
|
||||||
|
_success, button = event.get_button()
|
||||||
|
match button: # Xbox / Nintendo / PlayStation
|
||||||
|
case 304: # A / B / Circle
|
||||||
|
self._on_activate_button_pressed()
|
||||||
|
case 305: # B / A / Cross
|
||||||
|
self._on_return_button_pressed()
|
||||||
|
case 307: # Y / X / Triangle
|
||||||
|
self.window.search_entry.grab_focus()
|
||||||
|
case 308: # X / Y / Square
|
||||||
|
pass
|
||||||
|
case 310: # Left Shoulder Button
|
||||||
|
pass
|
||||||
|
case 311: # Right Shoulder Button
|
||||||
|
pass
|
||||||
|
case 314: # Back / - / Options
|
||||||
|
pass
|
||||||
|
case 315: # Start / + / Share
|
||||||
|
pass
|
||||||
|
case 544:
|
||||||
|
self._move_vertically(Gtk.DirectionType.UP)
|
||||||
|
case 545:
|
||||||
|
self._move_vertically(Gtk.DirectionType.DOWN)
|
||||||
|
case 546:
|
||||||
|
self._move_horizontally(Gtk.DirectionType.LEFT)
|
||||||
|
case 547:
|
||||||
|
self._move_horizontally(Gtk.DirectionType.RIGHT)
|
||||||
|
|
||||||
|
def _on_analog_axis_event(self, _device: Manette.Device, event: Manette.Event):
|
||||||
|
_, axis, value = event.get_absolute()
|
||||||
|
if abs(value) < STICK_DEADZONE:
|
||||||
|
return
|
||||||
|
|
||||||
|
match axis:
|
||||||
|
case 0:
|
||||||
|
direction = (
|
||||||
|
Gtk.DirectionType.LEFT if value < 0 else Gtk.DirectionType.RIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
if direction in self._allowed_inputs:
|
||||||
|
self._lock_input(direction)
|
||||||
|
self._move_horizontally(direction)
|
||||||
|
case 1:
|
||||||
|
direction = (
|
||||||
|
Gtk.DirectionType.UP if value < 0 else Gtk.DirectionType.DOWN
|
||||||
|
)
|
||||||
|
if direction in self._allowed_inputs:
|
||||||
|
self._lock_input(direction)
|
||||||
|
self._move_vertically(direction)
|
||||||
|
|
||||||
|
def _on_activate_button_pressed(self):
|
||||||
|
if self.window.navigation_view.props.visible_page_tag == "details":
|
||||||
|
if focus_widget := self.window.props.focus_widget:
|
||||||
|
focus_widget.activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._is_focused_on_top_bar() and (
|
||||||
|
focus_widget := self.window.props.focus_widget
|
||||||
|
):
|
||||||
|
if isinstance(focus_widget, Gtk.ToggleButton):
|
||||||
|
focus_widget.props.active = True
|
||||||
|
return
|
||||||
|
|
||||||
|
focus_widget.activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.window.grid.activate_action(
|
||||||
|
"list.activate-item",
|
||||||
|
GLib.Variant.new_uint32(self._get_current_position()),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_return_button_pressed(self):
|
||||||
|
if self.window.navigation_view.props.visible_page_tag == "details":
|
||||||
|
if self.window.details.stack.props.visible_child_name == "edit":
|
||||||
|
self.window.details.activate_action("details.cancel")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._is_focused_on_top_bar():
|
||||||
|
self.window.sort_button.props.active = False
|
||||||
|
return
|
||||||
|
|
||||||
|
self.window.navigation_view.pop_to_tag("games")
|
||||||
|
|
||||||
|
open_menu = self._get_active_menu_button()
|
||||||
|
|
||||||
|
if open_menu:
|
||||||
|
open_menu.set_active(False)
|
||||||
|
open_menu.grab_focus()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.window.grid.grab_focus()
|
||||||
|
self.window.props.focus_visible = True
|
||||||
|
|
||||||
|
def _navigate_to_game_position(self, new_pos: int):
|
||||||
|
if new_pos >= 0 and new_pos <= self._n_grid_games() - 1:
|
||||||
|
self.window.grid.scroll_to(new_pos, Gtk.ListScrollFlags.FOCUS, None)
|
||||||
|
else:
|
||||||
|
self.window.props.display.beep()
|
||||||
|
|
||||||
|
def _move_horizontally(self, direction: Gtk.DirectionType):
|
||||||
|
if self._is_focused_on_top_bar():
|
||||||
|
if not self.window.header_bar.child_focus(direction):
|
||||||
|
self.window.header_bar.keynav_failed(direction)
|
||||||
|
|
||||||
|
self.window.props.focus_visible = True
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._can_navigate_games_page():
|
||||||
|
self._navigate_to_game_position(
|
||||||
|
self._get_current_position()
|
||||||
|
+ (
|
||||||
|
-1
|
||||||
|
if direction
|
||||||
|
== self._get_rtl_direction(
|
||||||
|
Gtk.DirectionType.RIGHT, Gtk.DirectionType.LEFT
|
||||||
|
)
|
||||||
|
else 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.window.navigation_view.props.visible_page_tag == "details":
|
||||||
|
if self.window.details.stack.props.visible_child_name == "details":
|
||||||
|
self._navigate_action_buttons(direction)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
(focus_widget := self.window.props.focus_widget)
|
||||||
|
and (parent := focus_widget.props.parent)
|
||||||
|
and not parent.child_focus(direction)
|
||||||
|
):
|
||||||
|
parent.keynav_failed(direction)
|
||||||
|
|
||||||
|
def _move_vertically(self, direction: Gtk.DirectionType):
|
||||||
|
if self._is_focused_on_top_bar() and direction == Gtk.DirectionType.DOWN:
|
||||||
|
self.window.grid.grab_focus()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._can_navigate_games_page():
|
||||||
|
if not (game := self._get_focused_game()):
|
||||||
|
return
|
||||||
|
|
||||||
|
current_grid_columns = math.floor(
|
||||||
|
self.window.grid.get_width() / game.get_width()
|
||||||
|
)
|
||||||
|
|
||||||
|
new_pos = self._get_current_position() + (
|
||||||
|
-current_grid_columns
|
||||||
|
if direction == Gtk.DirectionType.UP
|
||||||
|
else current_grid_columns
|
||||||
|
)
|
||||||
|
if new_pos < 0 and direction == Gtk.DirectionType.UP:
|
||||||
|
self.window.search_entry.grab_focus()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._navigate_to_game_position(new_pos)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.window.navigation_view.props.visible_page_tag != "details":
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.window.details.stack.props.visible_child_name == "details":
|
||||||
|
self._navigate_action_buttons(direction)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not (focus_widget := self.window.props.focus_widget):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not (
|
||||||
|
current_row := (
|
||||||
|
focus_widget.get_ancestor(Adw.EntryRow)
|
||||||
|
or focus_widget.get_ancestor(Adw.WrapBox)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.window.header_bar.grab_focus()
|
||||||
|
if not (focus_widget := self.window.get_focus_child()):
|
||||||
|
return
|
||||||
|
|
||||||
|
if focus_widget.child_focus(direction):
|
||||||
|
self.window.props.focus_visible = True
|
||||||
|
return
|
||||||
|
|
||||||
|
focus_widget.keynav_failed(direction)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not (current_box := current_row.get_ancestor(Gtk.Box)):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not current_box.child_focus(direction):
|
||||||
|
current_box.keynav_failed(direction)
|
||||||
|
|
||||||
|
self.window.props.focus_visible = True
|
||||||
|
|
||||||
|
def _navigate_action_buttons(self, direction: Gtk.DirectionType):
|
||||||
|
if not (focus_widget := self.window.props.focus_widget):
|
||||||
|
return
|
||||||
|
|
||||||
|
widget = focus_widget
|
||||||
|
for _ in range(2): # Try to focus the actions, then try the play button
|
||||||
|
if not (widget := widget.props.parent):
|
||||||
|
break
|
||||||
|
|
||||||
|
if widget.child_focus(direction):
|
||||||
|
self.window.props.focus_visible = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# Focus on header bar if the user goes up/down
|
||||||
|
self.window.header_bar.grab_focus()
|
||||||
|
if not (focus_widget := self.window.get_focus_child()):
|
||||||
|
return
|
||||||
|
|
||||||
|
if focus_widget.child_focus(direction):
|
||||||
|
self.window.props.focus_visible = True
|
||||||
|
return
|
||||||
|
|
||||||
|
focus_widget.keynav_failed(direction)
|
||||||
|
|
||||||
|
def _get_active_menu_button(self) -> Gtk.MenuButton | None:
|
||||||
|
for button in self.window.main_menu_button, self.window.sort_button:
|
||||||
|
if button.props.active:
|
||||||
|
return button
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_focused_game(self) -> Gtk.Widget | None:
|
||||||
|
self.window.grid.grab_focus()
|
||||||
|
if (
|
||||||
|
focused_game := self.window.props.focus_widget
|
||||||
|
) and focused_game.get_ancestor(Gtk.GridView):
|
||||||
|
return focused_game
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_current_position(self) -> int:
|
||||||
|
if (game_widget := self._get_focused_game()) and isinstance(
|
||||||
|
item := game_widget.get_first_child(), GameItem
|
||||||
|
):
|
||||||
|
return item.position
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _can_navigate_games_page(self) -> bool:
|
||||||
|
return bool(
|
||||||
|
self.window.navigation_view.props.visible_page_tag == "games"
|
||||||
|
and self._n_grid_games()
|
||||||
|
and not self._get_active_menu_button()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_focused_on_top_bar(self) -> bool:
|
||||||
|
return bool(
|
||||||
|
self.window.header_bar.get_focus_child()
|
||||||
|
and not self._get_active_menu_button()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _n_grid_games(self) -> int:
|
||||||
|
return cast(Gtk.SingleSelection, self.window.grid.props.model).props.n_items
|
||||||
|
|
||||||
|
|
||||||
|
def _iterate_devices() -> Generator[Gamepad]:
|
||||||
|
monitor_iter = monitor.iterate()
|
||||||
|
has_next = True
|
||||||
|
while has_next:
|
||||||
|
has_next, device = monitor_iter.next()
|
||||||
|
if device:
|
||||||
|
yield Gamepad(device)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_device(device: Manette.Device):
|
||||||
|
model.remove(
|
||||||
|
next(
|
||||||
|
pos
|
||||||
|
for pos, gamepad in enumerate(model)
|
||||||
|
if cast(Gamepad, gamepad).device == device
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_window_style(model: Gio.ListStore):
|
||||||
|
if model.props.n_items > 0:
|
||||||
|
Gamepad.window.add_css_class("controller-connected")
|
||||||
|
else:
|
||||||
|
Gamepad.window.remove_css_class("controller-connected")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_monitor():
|
||||||
|
"""Connect monitor to device connect/disconnect signals."""
|
||||||
|
monitor.connect(
|
||||||
|
"device-connected",
|
||||||
|
lambda _, device: model.append(Gamepad(device)),
|
||||||
|
)
|
||||||
|
monitor.connect("device-disconnected", lambda _, device: _remove_device(device))
|
||||||
|
model.splice(0, 0, tuple(_iterate_devices()))
|
||||||
|
|
||||||
|
|
||||||
|
monitor = Manette.Monitor()
|
||||||
|
model = Gio.ListStore.new(Gamepad)
|
||||||
|
model.connect("items-changed", lambda model, *_: _update_window_style(model))
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
python.install_sources(
|
python.install_sources(
|
||||||
files('__init__.py', '__main__.py', 'application.py', 'games.py'),
|
files(
|
||||||
|
'__init__.py',
|
||||||
|
'__main__.py',
|
||||||
|
'application.py',
|
||||||
|
'games.py',
|
||||||
|
'gamepads.py',
|
||||||
|
),
|
||||||
subdir: 'cartridges',
|
subdir: 'cartridges',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class GameDetails(Adw.NavigationPage):
|
|||||||
__gtype_name__ = __qualname__
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
stack: Adw.ViewStack = Gtk.Template.Child()
|
stack: Adw.ViewStack = Gtk.Template.Child()
|
||||||
|
actions: Gtk.Box = Gtk.Template.Child()
|
||||||
name_entry: Adw.EntryRow = Gtk.Template.Child()
|
name_entry: Adw.EntryRow = Gtk.Template.Child()
|
||||||
developer_entry: Adw.EntryRow = Gtk.Template.Child()
|
developer_entry: Adw.EntryRow = Gtk.Template.Child()
|
||||||
executable_entry: Adw.EntryRow = Gtk.Template.Child()
|
executable_entry: Adw.EntryRow = Gtk.Template.Child()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* https://gitlab.gnome.org/World/highscore/-/blob/cea3c7492d0b3c78a8b79ca60e9a86e8cdd4ceaf/src/library/library.css#L86 */
|
/* https://gitlab.gnome.org/World/highscore/-/blob/cea3c7492d0b3c78a8b79ca60e9a86e8cdd4ceaf/src/library/library.css#L86 */
|
||||||
|
.controller-connected #grid > child:focus-within #cover,
|
||||||
#grid > child:focus-within:focus-visible #cover {
|
#grid > child:focus-within:focus-visible #cover {
|
||||||
outline: 5px solid rgb(from var(--accent-color) r g b / 50%);
|
outline: 5px solid rgb(from var(--accent-color) r g b / 50%);
|
||||||
}
|
}
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
|
.controller-connected #grid > child:focus-within #cover,
|
||||||
#grid > child:focus-within:focus-visible #cover {
|
#grid > child:focus-within:focus-visible #cover {
|
||||||
outline: 5px solid var(--accent-color);
|
outline: 5px solid var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Gtk 4.0;
|
|||||||
using Adw 1;
|
using Adw 1;
|
||||||
|
|
||||||
template $Window: Adw.ApplicationWindow {
|
template $Window: Adw.ApplicationWindow {
|
||||||
|
realize => $_setup_gamepad_monitor();
|
||||||
title: _("Cartridges");
|
title: _("Cartridges");
|
||||||
|
|
||||||
ShortcutController {
|
ShortcutController {
|
||||||
@@ -34,6 +35,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
content: Adw.NavigationView navigation_view {
|
content: Adw.NavigationView navigation_view {
|
||||||
Adw.NavigationPage {
|
Adw.NavigationPage {
|
||||||
title: bind template.title;
|
title: bind template.title;
|
||||||
|
tag: "games";
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
"view",
|
"view",
|
||||||
@@ -41,11 +43,11 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
|
|
||||||
child: Adw.ToolbarView {
|
child: Adw.ToolbarView {
|
||||||
[top]
|
[top]
|
||||||
Adw.HeaderBar {
|
Adw.HeaderBar header_bar {
|
||||||
title-widget: Adw.Clamp clamp {
|
title-widget: Adw.Clamp clamp {
|
||||||
tightening-threshold: bind clamp.maximum-size;
|
tightening-threshold: bind clamp.maximum-size;
|
||||||
|
|
||||||
child: CenterBox {
|
child: CenterBox title_box {
|
||||||
hexpand: true;
|
hexpand: true;
|
||||||
|
|
||||||
center-widget: SearchEntry search_entry {
|
center-widget: SearchEntry search_entry {
|
||||||
@@ -57,7 +59,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
stop-search => $_stop_search();
|
stop-search => $_stop_search();
|
||||||
};
|
};
|
||||||
|
|
||||||
end-widget: MenuButton {
|
end-widget: MenuButton sort_button {
|
||||||
icon-name: "filter-symbolic";
|
icon-name: "filter-symbolic";
|
||||||
tooltip-text: _("Sort & Filter");
|
tooltip-text: _("Sort & Filter");
|
||||||
margin-start: 6;
|
margin-start: 6;
|
||||||
@@ -113,7 +115,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[end]
|
[end]
|
||||||
MenuButton {
|
MenuButton main_menu_button {
|
||||||
icon-name: "open-menu-symbolic";
|
icon-name: "open-menu-symbolic";
|
||||||
tooltip-text: _("Main Menu");
|
tooltip-text: _("Main Menu");
|
||||||
primary: true;
|
primary: true;
|
||||||
@@ -128,15 +130,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
content: Adw.ToastOverlay toast_overlay {
|
content: Adw.ToastOverlay toast_overlay {
|
||||||
child: Adw.ViewStack {
|
child: Adw.ViewStack {
|
||||||
enable-transitions: true;
|
enable-transitions: true;
|
||||||
visible-child-name: bind $_if_else(
|
visible-child-name: bind $_if_else(grid.model as <NoSelection>.n-items, "grid", $_if_else(template.search-text, "empty-search", $_if_else(template.show-hidden, "empty-hidden", "empty") as <string>) as <string>) as <string>;
|
||||||
grid.model as <NoSelection>.n-items,
|
|
||||||
"grid",
|
|
||||||
$_if_else(
|
|
||||||
template.search-text,
|
|
||||||
"empty-search",
|
|
||||||
$_if_else(template.show-hidden, "empty-hidden", "empty") as <string>
|
|
||||||
) as <string>
|
|
||||||
) as <string>;
|
|
||||||
|
|
||||||
Adw.ViewStackPage {
|
Adw.ViewStackPage {
|
||||||
name: "grid";
|
name: "grid";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
|
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
||||||
|
|
||||||
|
import sys
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from typing import Any, TypeVar, cast
|
from typing import Any, TypeVar, cast
|
||||||
@@ -16,6 +17,10 @@ from cartridges.games import Game, GameSorter
|
|||||||
from .game_details import GameDetails
|
from .game_details import GameDetails
|
||||||
from .game_item import GameItem # noqa: F401
|
from .game_item import GameItem # noqa: F401
|
||||||
|
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
from cartridges import gamepads
|
||||||
|
from cartridges.gamepads import Gamepad
|
||||||
|
|
||||||
SORT_MODES = {
|
SORT_MODES = {
|
||||||
"last_played": ("last-played", True),
|
"last_played": ("last-played", True),
|
||||||
"a-z": ("name", False),
|
"a-z": ("name", False),
|
||||||
@@ -35,8 +40,12 @@ class Window(Adw.ApplicationWindow):
|
|||||||
__gtype_name__ = __qualname__
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
navigation_view: Adw.NavigationView = Gtk.Template.Child()
|
navigation_view: Adw.NavigationView = Gtk.Template.Child()
|
||||||
toast_overlay: Adw.ToastOverlay = Gtk.Template.Child()
|
header_bar: Adw.HeaderBar = Gtk.Template.Child()
|
||||||
|
title_box: Gtk.CenterBox = Gtk.Template.Child()
|
||||||
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
|
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
|
||||||
|
sort_button: Gtk.MenuButton = Gtk.Template.Child()
|
||||||
|
main_menu_button: Gtk.MenuButton = Gtk.Template.Child()
|
||||||
|
toast_overlay: Adw.ToastOverlay = Gtk.Template.Child()
|
||||||
grid: Gtk.GridView = Gtk.Template.Child()
|
grid: Gtk.GridView = Gtk.Template.Child()
|
||||||
sorter: GameSorter = Gtk.Template.Child()
|
sorter: GameSorter = Gtk.Template.Child()
|
||||||
details: GameDetails = Gtk.Template.Child()
|
details: GameDetails = Gtk.Template.Child()
|
||||||
@@ -67,7 +76,11 @@ class Window(Adw.ApplicationWindow):
|
|||||||
self.add_action(Gio.PropertyAction.new("show-hidden", self, "show-hidden"))
|
self.add_action(Gio.PropertyAction.new("show-hidden", self, "show-hidden"))
|
||||||
self.add_action_entries((
|
self.add_action_entries((
|
||||||
("search", lambda *_: self.search_entry.grab_focus()),
|
("search", lambda *_: self.search_entry.grab_focus()),
|
||||||
("edit", lambda _action, param, *_: self._edit(param.get_uint32()), "u"),
|
(
|
||||||
|
"edit",
|
||||||
|
lambda _action, param, *_: self._edit(param.get_uint32()),
|
||||||
|
"u",
|
||||||
|
),
|
||||||
("add", lambda *_: self._add()),
|
("add", lambda *_: self._add()),
|
||||||
("undo", lambda *_: self._undo()),
|
("undo", lambda *_: self._undo()),
|
||||||
))
|
))
|
||||||
@@ -88,6 +101,12 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
self.toast_overlay.add_toast(toast)
|
self.toast_overlay.add_toast(toast)
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _setup_gamepad_monitor(self, *_args):
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
Gamepad.window = self # pyright: ignore[reportPossiblyUnboundVariable]
|
||||||
|
gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable]
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T:
|
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T:
|
||||||
return first if condition else second
|
return first if condition else second
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--socket=fallback-x11",
|
"--socket=fallback-x11",
|
||||||
"--device=dri",
|
"--device=dri",
|
||||||
|
"--device=input",
|
||||||
"--socket=wayland",
|
"--socket=wayland",
|
||||||
"--talk-name=org.freedesktop.Flatpak",
|
"--talk-name=org.freedesktop.Flatpak",
|
||||||
"--filesystem=host:ro",
|
"--filesystem=host:ro",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
cartridges/application.py
|
cartridges/application.py
|
||||||
|
cartridges/gamepads.py
|
||||||
cartridges/games.py
|
cartridges/games.py
|
||||||
cartridges/sources/__init__.py
|
cartridges/sources/__init__.py
|
||||||
cartridges/sources/steam.py
|
cartridges/sources/steam.py
|
||||||
|
|||||||
Reference in New Issue
Block a user