diff --git a/cartridges/collections.py b/cartridges/collections.py index 8ad9258..236b6e7 100644 --- a/cartridges/collections.py +++ b/cartridges/collections.py @@ -2,14 +2,19 @@ # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel from collections.abc import Generator, Iterable -from typing import Any, cast +from gettext import gettext as _ +from typing import TYPE_CHECKING, Any, cast from gi.repository import Gio, GLib, GObject from cartridges import SETTINGS +if TYPE_CHECKING: + from .application import Application + from .ui.window import Window -class Collection(GObject.Object): + +class Collection(Gio.SimpleActionGroup): """Collection data class.""" __gtype_name__ = __qualname__ @@ -38,6 +43,27 @@ class Collection(GObject.Object): lambda _, name: f"{name}-symbolic", ) + self.add_action(remove := Gio.SimpleAction.new("remove")) + remove.connect("activate", lambda *_: self._remove()) + self.bind_property( + "in-model", + remove, + "enabled", + GObject.BindingFlags.SYNC_CREATE, + ) + + def _remove(self): + self.removed = True + save() + + app = cast("Application", Gio.Application.get_default()) + window = cast("Window", app.props.active_window) + window.send_toast(_("{} removed").format(self.name), undo=self._undo_remove) + + def _undo_remove(self): + self.removed = False + save() + def _get_collections() -> Generator[Collection]: for data in SETTINGS.get_value("collections").unpack(): diff --git a/cartridges/ui/collection-details.blp b/cartridges/ui/collection-details.blp index c569228..1b3ad80 100644 --- a/cartridges/ui/collection-details.blp +++ b/cartridges/ui/collection-details.blp @@ -2,11 +2,18 @@ using Gtk 4.0; using Adw 1; template $CollectionDetails: Adw.Dialog { - title: _("New Collection"); + title: bind $_or(template.collection as <$Collection>.name, _("New Collection")) as ; content-width: 360; default-widget: apply_button; focus-widget: name_entry; + ShortcutController { + Shortcut { + trigger: "Delete|KP_Delete"; + action: "action(collection.remove)"; + } + } + child: Adw.ToolbarView { [top] Adw.HeaderBar { @@ -24,7 +31,7 @@ template $CollectionDetails: Adw.Dialog { Button apply_button { action-name: "details.apply"; icon-name: "apply-symbolic"; - tooltip-text: _("Add"); + tooltip-text: bind $_if_else(template.collection as <$Collection>.in-model, _("Apply"), _("Add")) as ; styles [ "suggested-action", @@ -36,6 +43,7 @@ template $CollectionDetails: Adw.Dialog { Adw.PreferencesGroup { Adw.EntryRow name_entry { title: _("Name"); + text: bind template.collection as <$Collection>.name; activates-default: true; } } @@ -53,6 +61,19 @@ template $CollectionDetails: Adw.Dialog { "card", ] } + + Adw.PreferencesGroup { + visible: bind remove_row.sensitive; + + Adw.ButtonRow remove_row { + title: _("Remove"); + action-name: "collection.remove"; + + styles [ + "destructive-action", + ] + } + } }; }; } diff --git a/cartridges/ui/collection_details.py b/cartridges/ui/collection_details.py index ef9c721..3c8b293 100644 --- a/cartridges/ui/collection_details.py +++ b/cartridges/ui/collection_details.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel -from typing import Any, cast +from typing import Any, TypeVar, cast from gi.repository import Adw, Gio, GObject, Gtk @@ -33,6 +33,8 @@ ICONS = ( "fist", ) +_T = TypeVar("_T") + @Gtk.Template.from_resource(f"{PREFIX}/collection-details.ui") class CollectionDetails(Adw.Dialog): @@ -43,7 +45,19 @@ class CollectionDetails(Adw.Dialog): name_entry: Adw.EntryRow = Gtk.Template.Child() icons_box: Gtk.FlowBox = Gtk.Template.Child() - collection = GObject.Property(type=Collection) + sort_changed = GObject.Signal() + + @GObject.Property(type=Collection) + def collection(self) -> Collection: + """The collection that `self` represents.""" + return self._collection + + @collection.setter + 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): super().__init__(**kwargs) @@ -77,7 +91,12 @@ class CollectionDetails(Adw.Dialog): ) def _apply(self): - self.collection.name = self.name_entry.props.text + name = self.name_entry.props.text + if self.collection.name != name: + self.collection.name = name + if self.collection.in_model: + self.emit("sort-changed") + self.collection.icon = ICONS[ self.icons_box.get_selected_children()[0].get_index() ] @@ -88,3 +107,11 @@ class CollectionDetails(Adw.Dialog): collections.save() self.close() + + @Gtk.Template.Callback() + def _or(self, _obj, first: _T, second: _T) -> _T: + return first or second + + @Gtk.Template.Callback() + def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T: + return first if condition else second diff --git a/cartridges/ui/window.blp b/cartridges/ui/window.blp index 3b60d3f..282552c 100644 --- a/cartridges/ui/window.blp +++ b/cartridges/ui/window.blp @@ -90,6 +90,8 @@ template $Window: Adw.ApplicationWindow { Adw.SidebarSection collections { title: _("Collections"); + + menu-model: menu collection_menu {}; } Adw.SidebarSection { diff --git a/cartridges/ui/window.py b/cartridges/ui/window.py index a37abae..bd41159 100644 --- a/cartridges/ui/window.py +++ b/cartridges/ui/window.py @@ -48,8 +48,10 @@ class Window(Adw.ApplicationWindow): __gtype_name__ = __qualname__ split_view: Adw.OverlaySplitView = Gtk.Template.Child() + sidebar: Adw.Sidebar = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue] collections: Adw.SidebarSection = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue] new_collection_item: Adw.SidebarItem = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue] + collection_menu: Gio.Menu = Gtk.Template.Child() navigation_view: Adw.NavigationView = Gtk.Template.Child() header_bar: Adw.HeaderBar = Gtk.Template.Child() title_box: Gtk.CenterBox = Gtk.Template.Child() @@ -63,10 +65,11 @@ class Window(Adw.ApplicationWindow): search_text = GObject.Property(type=str) show_hidden = GObject.Property(type=bool, default=False) - collection = GObject.Property(type=Collection) settings = GObject.Property(type=Gtk.Settings) + _collection: Collection | None = None + _collection_removed_signal: int | None = None _selected_sidebar_item = 0 @GObject.Property(type=Gio.ListStore) @@ -74,6 +77,27 @@ class Window(Adw.ApplicationWindow): """Model of the user's games.""" return games.model + @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) @@ -106,6 +130,16 @@ class Window(Adw.ApplicationWindow): "u", ), ("add", lambda *_: self._add()), + ( + "edit-collection", + lambda _action, param, *_: self._edit_collection(param.get_uint32()), + "u", + ), + ( + "remove-collection", + lambda _action, param, *_: self._remove_collection(param.get_uint32()), + "u", + ), ("undo", lambda *_: self._undo()), )) @@ -152,6 +186,20 @@ class Window(Adw.ApplicationWindow): sidebar.props.selected = self._selected_sidebar_item self._selected_sidebar_item = sidebar.props.selected + @Gtk.Template.Callback() + def _setup_sidebar_menu(self, _sidebar, item: Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue] + if isinstance(item, CollectionSidebarItem): + menu = self.collection_menu + menu.remove_all() + menu.append( + _("Edit"), + f"win.edit-collection(uint32 {item.get_section_index()})", + ) + menu.append( + _("Remove"), + f"win.remove-collection(uint32 {item.get_section_index()})", + ) + @Gtk.Template.Callback() def _setup_gamepad_monitor(self, *_args): if sys.platform.startswith("linux"): @@ -210,6 +258,19 @@ class Window(Adw.ApplicationWindow): details = CollectionDetails(collection=Collection()) details.present(self) + def _edit_collection(self, pos: int): + collection = self.collections.get_item(pos).collection + details = CollectionDetails(collection=collection) + details.connect( + "sort-changed", + lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT), + ) + details.present(self) + + def _remove_collection(self, pos: int): + collection = self.collections.get_item(pos).collection + collection.activate_action("remove") + def _undo(self, toast: Adw.Toast | None = None): if toast: self._history.pop(toast)()