From 515bafa428d191916ca1555c7b1dc3c4d5cd755e Mon Sep 17 00:00:00 2001 From: Jamie Gravendeel Date: Fri, 9 Jan 2026 12:45:34 +0100 Subject: [PATCH] ui: Use GObject.SignalGroup --- cartridges/ui/collection-details.blp | 6 +++ cartridges/ui/collection_details.py | 14 +++++-- cartridges/ui/game-details.blp | 6 +++ cartridges/ui/game_details.py | 22 ++++------ cartridges/ui/sources.py | 20 ++++++--- cartridges/ui/window.blp | 12 ++++++ cartridges/ui/window.py | 63 ++++++++++++---------------- 7 files changed, 85 insertions(+), 58 deletions(-) diff --git a/cartridges/ui/collection-details.blp b/cartridges/ui/collection-details.blp index 4ec74f7..778364b 100644 --- a/cartridges/ui/collection-details.blp +++ b/cartridges/ui/collection-details.blp @@ -1,5 +1,11 @@ using Gtk 4.0; using Adw 1; +using GObject 2.0; + +GObject.SignalGroup collection_signals { + target: bind template.collection; + target-type: typeof<$Collection>; +} template $CollectionDetails: Adw.Dialog { title: bind $_or(template.collection as <$Collection>.name, _("New Collection")) as ; diff --git a/cartridges/ui/collection_details.py b/cartridges/ui/collection_details.py index 8f338ac..4c3165d 100644 --- a/cartridges/ui/collection_details.py +++ b/cartridges/ui/collection_details.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel from itertools import product -from typing import Any, NamedTuple, cast +from typing import Any, NamedTuple from gi.repository import Adw, Gio, GObject, Gtk @@ -50,6 +50,7 @@ class CollectionDetails(Adw.Dialog): name_entry: Adw.EntryRow = Gtk.Template.Child() icons_grid: Gtk.Grid = Gtk.Template.Child() + collection_signals: GObject.SignalGroup = Gtk.Template.Child() sort_changed = GObject.Signal() _selected_icon: str @@ -63,10 +64,8 @@ class CollectionDetails(Adw.Dialog): def collection(self, collection: Collection): self._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) self.insert_action_group("details", group := Gio.SimpleActionGroup()) @@ -81,6 +80,13 @@ class CollectionDetails(Adw.Dialog): 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 for index, (row, col) in enumerate(product(range(3), range(7))): icon = _ICONS[index].name diff --git a/cartridges/ui/game-details.blp b/cartridges/ui/game-details.blp index b56800b..7413b07 100644 --- a/cartridges/ui/game-details.blp +++ b/cartridges/ui/game-details.blp @@ -1,6 +1,12 @@ using Gtk 4.0; using Gdk 4.0; using Adw 1; +using GObject 2.0; + +GObject.SignalGroup game_signals { + target: bind template.game; + target-type: typeof<$Game>; +} template $GameDetails: Adw.NavigationPage { name: "details"; diff --git a/cartridges/ui/game_details.py b/cartridges/ui/game_details.py index 314ee2e..38c9e66 100644 --- a/cartridges/ui/game_details.py +++ b/cartridges/ui/game_details.py @@ -20,7 +20,7 @@ from cartridges.sources import imported from .collections import CollectionsBox 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} _REQUIRED_PROPERTIES = { 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() executable_entry: Adw.EntryRow = Gtk.Template.Child() + game_signals: GObject.SignalGroup = Gtk.Template.Child() sort_changed = GObject.Signal() @GObject.Property(type=Game) @@ -52,21 +53,9 @@ class GameDetails(Adw.NavigationPage): self._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): super().__init__(**kwargs) - self._signal_ids = dict[Gio.SimpleAction, int]() - self.insert_action_group("details", group := Gio.SimpleActionGroup()) group.add_action_entries(( ("edit", lambda *_: self.edit()), @@ -100,6 +89,13 @@ class GameDetails(Adw.NavigationPage): valid = Gtk.ClosureExpression.new(bool, lambda _, *values: all(values), entries) 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): """Enter edit mode.""" for prop in _EDITABLE_PROPERTIES: diff --git a/cartridges/ui/sources.py b/cartridges/ui/sources.py index d8e58c7..e0c11f5 100644 --- a/cartridges/ui/sources.py +++ b/cartridges/ui/sources.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel +from typing import Any + from gi.repository import Adw, Gio, GObject, Gtk from cartridges import sources @@ -30,13 +32,21 @@ class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAcce filter=games.filter_, watch_items=True, # pyright: ignore[reportCallIssue] ) - # https://gitlab.gnome.org/GNOME/gtk/-/issues/7959 - self.model.connect( - "items-changed", - lambda *_: self.set_property("visible", self.model.props.n_items), - ) self.props.visible = self.model.props.n_items + def __init__(self, source: Source, **kwargs: Any): + super().__init__(**kwargs) + + # https://gitlab.gnome.org/GNOME/gtk/-/issues/7959 + self._model_signals = GObject.SignalGroup.new(Gio.ListModel) + self._model_signals.connect_closure( + "items-changed", + lambda model, *_: self.set_property("visible", model.props.n_items), + after=True, + ) + self.bind_property("model", self._model_signals, "target") + self.source = source + model = Gtk.SortListModel.new( sources.model, diff --git a/cartridges/ui/window.blp b/cartridges/ui/window.blp index a872c7b..8d5236b 100644 --- a/cartridges/ui/window.blp +++ b/cartridges/ui/window.blp @@ -1,5 +1,17 @@ using Gtk 4.0; 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; +} template $Window: Adw.ApplicationWindow { realize => $_setup_gamepad_monitor(); diff --git a/cartridges/ui/window.py b/cartridges/ui/window.py index 3df39ed..2789c58 100644 --- a/cartridges/ui/window.py +++ b/cartridges/ui/window.py @@ -13,7 +13,7 @@ from gi.repository import Adw, Gio, GLib, GObject, Gtk from cartridges import STATE_SETTINGS from cartridges.collections import Collection 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 .collection_details import CollectionDetails @@ -62,38 +62,18 @@ class Window(Adw.ApplicationWindow): collection_filter: CollectionFilter = 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) show_hidden = GObject.Property(type=bool, default=False) settings = GObject.Property(type=Gtk.Settings) - _collection: Collection | None = None - _collection_removed_signal: int | None = None _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): super().__init__(**kwargs) @@ -110,7 +90,10 @@ class Window(Adw.ApplicationWindow): # https://gitlab.gnome.org/GNOME/gtk/-/issues/7901 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( collections.model, lambda collection: CollectionSidebarItem(collection=collection), @@ -149,6 +132,18 @@ class Window(Adw.ApplicationWindow): ("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] = {} def send_toast(self, title: str, *, undo: _UndoFunc | None = None): @@ -165,15 +160,11 @@ class Window(Adw.ApplicationWindow): self.toast_overlay.add_toast(toast) - def _create_source_item(self, source: Source) -> SourceSidebarItem: - item = SourceSidebarItem(source=source) - item.connect( - "notify::visible", - lambda item, _: self._source_empty() if not item.props.visible else None, - ) - return item + def _collection_removed(self): + self.collection = None + self.sidebar.props.selected = 0 - def _source_empty(self): + def _model_emptied(self): self.model = games.model self.sidebar.props.selected = 0 @@ -285,12 +276,12 @@ class Window(Adw.ApplicationWindow): if game_id: collection.game_ids.add(game_id) - details = CollectionDetails(collection=collection) + details = CollectionDetails(collection) details.present(self) def _edit_collection(self, pos: int): collection = self.collections.get_item(pos).collection - details = CollectionDetails(collection=collection) + details = CollectionDetails(collection) details.connect( "sort-changed", lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT),