Compare commits

...

2 Commits

Author SHA1 Message Date
Jamie Gravendeel
dab108ce8b ui: Use GObject.BindingGroup 2026-01-10 01:46:20 +01:00
Jamie Gravendeel
515bafa428 ui: Use GObject.SignalGroup 2026-01-10 01:22:55 +01:00
8 changed files with 105 additions and 85 deletions

View File

@@ -1,5 +1,11 @@
using Gtk 4.0; using Gtk 4.0;
using Adw 1; using Adw 1;
using GObject 2.0;
GObject.SignalGroup collection_signals {
target: bind template.collection;
target-type: typeof<$Collection>;
}
template $CollectionDetails: Adw.Dialog { template $CollectionDetails: Adw.Dialog {
title: bind $_or(template.collection as <$Collection>.name, _("New Collection")) as <string>; title: bind $_or(template.collection as <$Collection>.name, _("New Collection")) as <string>;

View File

@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from itertools import product from itertools import product
from typing import Any, NamedTuple, cast from typing import Any, NamedTuple
from gi.repository import Adw, Gio, GObject, Gtk from gi.repository import Adw, Gio, GObject, Gtk
@@ -50,6 +50,7 @@ class CollectionDetails(Adw.Dialog):
name_entry: Adw.EntryRow = Gtk.Template.Child() name_entry: Adw.EntryRow = Gtk.Template.Child()
icons_grid: Gtk.Grid = Gtk.Template.Child() icons_grid: Gtk.Grid = Gtk.Template.Child()
collection_signals: GObject.SignalGroup = Gtk.Template.Child()
sort_changed = GObject.Signal() sort_changed = GObject.Signal()
_selected_icon: str _selected_icon: str
@@ -63,10 +64,8 @@ class CollectionDetails(Adw.Dialog):
def collection(self, collection: Collection): def collection(self, collection: Collection):
self._collection = collection self._collection = collection
self.insert_action_group("collection", collection) self.insert_action_group("collection", collection)
remove_action = cast(Gio.SimpleAction, collection.lookup_action("remove"))
remove_action.connect("activate", lambda *_: self.force_close())
def __init__(self, **kwargs: Any): def __init__(self, collection: Collection, **kwargs: Any):
super().__init__(**kwargs) super().__init__(**kwargs)
self.insert_action_group("details", group := Gio.SimpleActionGroup()) self.insert_action_group("details", group := Gio.SimpleActionGroup())
@@ -81,6 +80,13 @@ class CollectionDetails(Adw.Dialog):
transform_to=lambda _, text: bool(text), transform_to=lambda _, text: bool(text),
) )
self.collection_signals.connect_closure(
"notify::removed",
lambda *_: self.force_close(),
after=True,
)
self.collection = collection
group_button = None group_button = None
for index, (row, col) in enumerate(product(range(3), range(7))): for index, (row, col) in enumerate(product(range(3), range(7))):
icon = _ICONS[index].name icon = _ICONS[index].name

View File

@@ -37,19 +37,9 @@ class CollectionFilter(Gtk.Filter):
class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue] class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
"""A sidebar item representing a collection.""" """A sidebar item representing a collection."""
@GObject.Property(type=Collection) collection = GObject.Property(type=Collection)
def collection(self) -> Collection:
"""The collection that `self` represents."""
return self._collection
@collection.setter def __init__(self, collection: Collection, **kwargs: Any):
def collection(self, collection: Collection):
self._collection = collection
flags = GObject.BindingFlags.SYNC_CREATE
collection.bind_property("name", self, "title", flags)
collection.bind_property("icon-name", self, "icon-name", flags)
def __init__(self, **kwargs: Any):
super().__init__(**kwargs) super().__init__(**kwargs)
self.bind_property( self.bind_property(
@@ -60,6 +50,13 @@ class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttribute
lambda _, name: GLib.markup_escape_text(name), lambda _, name: GLib.markup_escape_text(name),
) )
self._collection_bindings = GObject.BindingGroup()
flags = GObject.BindingFlags.DEFAULT
self._collection_bindings.bind("name", self, "title", flags)
self._collection_bindings.bind("icon-name", self, "icon-name", flags)
self.bind_property("collection", self._collection_bindings, "source")
self.collection = collection
class CollectionButton(Gtk.ToggleButton): class CollectionButton(Gtk.ToggleButton):
"""A toggle button representing a collection.""" """A toggle button representing a collection."""

View File

@@ -1,6 +1,12 @@
using Gtk 4.0; using Gtk 4.0;
using Gdk 4.0; using Gdk 4.0;
using Adw 1; using Adw 1;
using GObject 2.0;
GObject.SignalGroup game_signals {
target: bind template.game;
target-type: typeof<$Game>;
}
template $GameDetails: Adw.NavigationPage { template $GameDetails: Adw.NavigationPage {
name: "details"; name: "details";

View File

@@ -20,7 +20,7 @@ from cartridges.sources import imported
from .collections import CollectionsBox from .collections import CollectionsBox
from .cover import Cover # noqa: F401 from .cover import Cover # noqa: F401
_POP_ON_ACTION = "hide", "unhide", "remove" _POP_ON_PROPERTY_NOTIFY = "hidden", "removed"
_EDITABLE_PROPERTIES = {prop.name for prop in games.PROPERTIES if prop.editable} _EDITABLE_PROPERTIES = {prop.name for prop in games.PROPERTIES if prop.editable}
_REQUIRED_PROPERTIES = { _REQUIRED_PROPERTIES = {
prop.name for prop in games.PROPERTIES if prop.editable and prop.required prop.name for prop in games.PROPERTIES if prop.editable and prop.required
@@ -40,6 +40,7 @@ class GameDetails(Adw.NavigationPage):
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()
game_signals: GObject.SignalGroup = Gtk.Template.Child()
sort_changed = GObject.Signal() sort_changed = GObject.Signal()
@GObject.Property(type=Game) @GObject.Property(type=Game)
@@ -52,21 +53,9 @@ class GameDetails(Adw.NavigationPage):
self._game = game self._game = game
self.insert_action_group("game", game) self.insert_action_group("game", game)
for action, ident in self._signal_ids.copy().items():
action.disconnect(ident)
del self._signal_ids[action]
for name in _POP_ON_ACTION:
action = cast(Gio.SimpleAction, game.lookup_action(name))
self._signal_ids[action] = action.connect(
"activate", lambda *_: self.activate_action("navigation.pop")
)
def __init__(self, **kwargs: Any): def __init__(self, **kwargs: Any):
super().__init__(**kwargs) super().__init__(**kwargs)
self._signal_ids = dict[Gio.SimpleAction, int]()
self.insert_action_group("details", group := Gio.SimpleActionGroup()) self.insert_action_group("details", group := Gio.SimpleActionGroup())
group.add_action_entries(( group.add_action_entries((
("edit", lambda *_: self.edit()), ("edit", lambda *_: self.edit()),
@@ -100,6 +89,13 @@ class GameDetails(Adw.NavigationPage):
valid = Gtk.ClosureExpression.new(bool, lambda _, *values: all(values), entries) valid = Gtk.ClosureExpression.new(bool, lambda _, *values: all(values), entries)
valid.bind(apply, "enabled") valid.bind(apply, "enabled")
for name in _POP_ON_PROPERTY_NOTIFY:
self.game_signals.connect_closure(
f"notify::{name}",
lambda *_: self.activate_action("navigation.pop"),
after=True,
)
def edit(self): def edit(self):
"""Enter edit mode.""" """Enter edit mode."""
for prop in _EDITABLE_PROPERTIES: for prop in _EDITABLE_PROPERTIES:

View File

@@ -1,6 +1,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from typing import Any
from gi.repository import Adw, Gio, GObject, Gtk from gi.repository import Adw, Gio, GObject, Gtk
from cartridges import sources from cartridges import sources
@@ -11,31 +13,35 @@ from cartridges.ui import games
class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue] class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
"""A sidebar item representing a source.""" """A sidebar item representing a source."""
source = GObject.Property(type=Source)
model = GObject.Property(type=Gio.ListModel) model = GObject.Property(type=Gio.ListModel)
@GObject.Property(type=Source) def __init__(self, source: Source, **kwargs: Any):
def source(self) -> Source: super().__init__(**kwargs)
"""The source that `self` represents."""
return self._source
@source.setter
def source(self, source: Source):
self._source = source
flags = GObject.BindingFlags.SYNC_CREATE
source.bind_property("name", self, "title", flags)
source.bind_property("icon-name", self, "icon-name", flags)
self.model = Gtk.FilterListModel(
model=source,
filter=games.filter_,
watch_items=True, # pyright: ignore[reportCallIssue]
)
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7959 # https://gitlab.gnome.org/GNOME/gtk/-/issues/7959
self.model.connect( self._model_signals = GObject.SignalGroup.new(Gio.ListModel)
self._model_signals.connect_closure(
"items-changed", "items-changed",
lambda *_: self.set_property("visible", self.model.props.n_items), lambda model, *_: model.notify("n-items"),
after=True,
) )
self.props.visible = self.model.props.n_items
self._source_bindings = GObject.BindingGroup()
flags = GObject.BindingFlags.DEFAULT
self._source_bindings.bind("name", self, "title", flags)
self._source_bindings.bind("icon-name", self, "icon-name", flags)
self.bind_property("source", self._source_bindings, "source")
self.model = Gtk.FilterListModel(filter=games.filter_, watch_items=True) # pyright: ignore[reportCallIssue]
self.model.bind_property(
"n-items",
self,
"visible",
GObject.BindingFlags.SYNC_CREATE,
)
self.bind_property("source", self.model, "model")
self.source = source
model = Gtk.SortListModel.new( model = Gtk.SortListModel.new(

View File

@@ -1,5 +1,17 @@
using Gtk 4.0; using Gtk 4.0;
using Adw 1; using Adw 1;
using GObject 2.0;
using Gio 2.0;
GObject.SignalGroup collection_signals {
target: bind template.collection;
target-type: typeof<$Collection>;
}
GObject.SignalGroup model_signals {
target: bind template.model;
target-type: typeof<Gio.ListModel>;
}
template $Window: Adw.ApplicationWindow { template $Window: Adw.ApplicationWindow {
realize => $_setup_gamepad_monitor(); realize => $_setup_gamepad_monitor();

View File

@@ -13,7 +13,7 @@ from gi.repository import Adw, Gio, GLib, GObject, Gtk
from cartridges import STATE_SETTINGS from cartridges import STATE_SETTINGS
from cartridges.collections import Collection from cartridges.collections import Collection
from cartridges.config import PREFIX, PROFILE from cartridges.config import PREFIX, PROFILE
from cartridges.sources import Source, imported from cartridges.sources import imported
from cartridges.ui import collections, games, sources from cartridges.ui import collections, games, sources
from .collection_details import CollectionDetails from .collection_details import CollectionDetails
@@ -62,38 +62,18 @@ class Window(Adw.ApplicationWindow):
collection_filter: CollectionFilter = Gtk.Template.Child() collection_filter: CollectionFilter = Gtk.Template.Child()
details: GameDetails = Gtk.Template.Child() details: GameDetails = Gtk.Template.Child()
model = GObject.Property(type=Gio.ListModel, default=games.model) collection = GObject.Property(type=Collection)
collection_signals: GObject.SignalGroup = Gtk.Template.Child()
model = GObject.Property(type=Gio.ListModel)
model_signals: GObject.SignalGroup = Gtk.Template.Child()
search_text = GObject.Property(type=str) search_text = GObject.Property(type=str)
show_hidden = GObject.Property(type=bool, default=False) show_hidden = GObject.Property(type=bool, default=False)
settings = GObject.Property(type=Gtk.Settings) settings = GObject.Property(type=Gtk.Settings)
_collection: Collection | None = None
_collection_removed_signal: int | None = None
_selected_sidebar_item = 0 _selected_sidebar_item = 0
@GObject.Property(type=Collection)
def collection(self) -> Collection | None:
"""The currently selected collection."""
return self._collection
@collection.setter
def collection(self, collection: Collection | None):
if self._collection and self._collection_removed_signal:
self._collection.disconnect(self._collection_removed_signal)
self._collection = collection
self._collection_removed_signal = (
collection.connect("notify::removed", lambda *_: self._collection_removed())
if collection
else None
)
def _collection_removed(self):
self.collection = None
self.sidebar.props.selected = 0
def __init__(self, **kwargs: Any): def __init__(self, **kwargs: Any):
super().__init__(**kwargs) super().__init__(**kwargs)
@@ -110,10 +90,13 @@ class Window(Adw.ApplicationWindow):
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901 # https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
self.search_entry.set_key_capture_widget(self) self.search_entry.set_key_capture_widget(self)
self.sources.bind_model(sources.model, self._create_source_item) self.sources.bind_model(
sources.model,
lambda source: SourceSidebarItem(source),
)
self.collections.bind_model( self.collections.bind_model(
collections.model, collections.model,
lambda collection: CollectionSidebarItem(collection=collection), lambda collection: CollectionSidebarItem(collection),
) )
self.add_action(STATE_SETTINGS.create_action("show-sidebar")) self.add_action(STATE_SETTINGS.create_action("show-sidebar"))
@@ -149,6 +132,18 @@ class Window(Adw.ApplicationWindow):
("undo", lambda *_: self._undo()), ("undo", lambda *_: self._undo()),
)) ))
self.collection_signals.connect_closure(
"notify::removed",
lambda *_: self._collection_removed(),
after=True,
)
self.model_signals.connect_closure(
"items-changed",
lambda model, *_: None if model else self._model_emptied(),
after=True,
)
self.model = games.model
self._history: dict[Adw.Toast, _UndoFunc] = {} self._history: dict[Adw.Toast, _UndoFunc] = {}
def send_toast(self, title: str, *, undo: _UndoFunc | None = None): def send_toast(self, title: str, *, undo: _UndoFunc | None = None):
@@ -165,15 +160,11 @@ class Window(Adw.ApplicationWindow):
self.toast_overlay.add_toast(toast) self.toast_overlay.add_toast(toast)
def _create_source_item(self, source: Source) -> SourceSidebarItem: def _collection_removed(self):
item = SourceSidebarItem(source=source) self.collection = None
item.connect( self.sidebar.props.selected = 0
"notify::visible",
lambda item, _: self._source_empty() if not item.props.visible else None,
)
return item
def _source_empty(self): def _model_emptied(self):
self.model = games.model self.model = games.model
self.sidebar.props.selected = 0 self.sidebar.props.selected = 0
@@ -285,12 +276,12 @@ class Window(Adw.ApplicationWindow):
if game_id: if game_id:
collection.game_ids.add(game_id) collection.game_ids.add(game_id)
details = CollectionDetails(collection=collection) details = CollectionDetails(collection)
details.present(self) details.present(self)
def _edit_collection(self, pos: int): def _edit_collection(self, pos: int):
collection = self.collections.get_item(pos).collection collection = self.collections.get_item(pos).collection
details = CollectionDetails(collection=collection) details = CollectionDetails(collection)
details.connect( details.connect(
"sort-changed", "sort-changed",
lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT), lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT),