collection-details: Support editing and removing

This commit is contained in:
Jamie Gravendeel
2025-12-22 14:44:21 +01:00
parent 62004753b6
commit 7d7999d8a9
5 changed files with 145 additions and 8 deletions

View File

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

View File

@@ -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 <string>;
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 <string>;
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",
]
}
}
};
};
}

View File

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

View File

@@ -90,6 +90,8 @@ template $Window: Adw.ApplicationWindow {
Adw.SidebarSection collections {
title: _("Collections");
menu-model: menu collection_menu {};
}
Adw.SidebarSection {

View File

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