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 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 <string>;

View File

@@ -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

View File

@@ -37,19 +37,9 @@ class CollectionFilter(Gtk.Filter):
class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
"""A sidebar item representing a collection."""
@GObject.Property(type=Collection)
def collection(self) -> Collection:
"""The collection that `self` represents."""
return self._collection
collection = GObject.Property(type=Collection)
@collection.setter
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):
def __init__(self, collection: Collection, **kwargs: Any):
super().__init__(**kwargs)
self.bind_property(
@@ -60,6 +50,13 @@ class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttribute
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):
"""A toggle button representing a collection."""

View File

@@ -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";

View File

@@ -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:

View File

@@ -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
@@ -11,31 +13,35 @@ from cartridges.ui import games
class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
"""A sidebar item representing a source."""
source = GObject.Property(type=Source)
model = GObject.Property(type=Gio.ListModel)
@GObject.Property(type=Source)
def source(self) -> Source:
"""The source that `self` represents."""
return self._source
def __init__(self, source: Source, **kwargs: Any):
super().__init__(**kwargs)
@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
self.model.connect(
self._model_signals = GObject.SignalGroup.new(Gio.ListModel)
self._model_signals.connect_closure(
"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(

View File

@@ -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<Gio.ListModel>;
}
template $Window: Adw.ApplicationWindow {
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.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,10 +90,13 @@ 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),
lambda collection: CollectionSidebarItem(collection),
)
self.add_action(STATE_SETTINGS.create_action("show-sidebar"))
@@ -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),