From c5cfa476fffabf703e60780dc160f21e3f5897ba Mon Sep 17 00:00:00 2001 From: Jamie Gravendeel Date: Mon, 29 Dec 2025 21:40:54 +0100 Subject: [PATCH] sources: Add initial UI --- cartridges/sources/__init__.py | 2 ++ cartridges/ui/meson.build | 1 + cartridges/ui/sources.py | 44 +++++++++++++++++++++++++++++++ cartridges/ui/window.blp | 4 +++ cartridges/ui/window.py | 24 +++++++++++++++-- data/icons/heroic-symbolic.svg | 1 + data/icons/icons.gresource.xml.in | 4 +++ data/icons/imported-symbolic.svg | 1 + data/icons/steam-symbolic.svg | 1 + po/POTFILES.in | 1 + 10 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 cartridges/ui/sources.py create mode 100644 data/icons/heroic-symbolic.svg create mode 100644 data/icons/imported-symbolic.svg create mode 100644 data/icons/steam-symbolic.svg diff --git a/cartridges/sources/__init__.py b/cartridges/sources/__init__.py index bad9b62..ce09a22 100644 --- a/cartridges/sources/__init__.py +++ b/cartridges/sources/__init__.py @@ -54,6 +54,8 @@ class _SourceModule(Protocol): class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatibleMethodOverride] """A source of games to import.""" + __gtype_name__ = __qualname__ + id = GObject.Property(type=str) name = GObject.Property(type=str) icon_name = GObject.Property(type=str) diff --git a/cartridges/ui/meson.build b/cartridges/ui/meson.build index e92caf3..b9eb6de 100644 --- a/cartridges/ui/meson.build +++ b/cartridges/ui/meson.build @@ -7,6 +7,7 @@ python.install_sources( 'game_details.py', 'game_item.py', 'games.py', + 'sources.py', 'window.py', ), subdir: 'cartridges' / 'ui', diff --git a/cartridges/ui/sources.py b/cartridges/ui/sources.py new file mode 100644 index 0000000..d8e58c7 --- /dev/null +++ b/cartridges/ui/sources.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel + +from gi.repository import Adw, Gio, GObject, Gtk + +from cartridges import sources +from cartridges.sources import Source +from cartridges.ui import games + + +class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue] + """A sidebar item representing a source.""" + + model = GObject.Property(type=Gio.ListModel) + + @GObject.Property(type=Source) + def source(self) -> Source: + """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 + self.model.connect( + "items-changed", + lambda *_: self.set_property("visible", self.model.props.n_items), + ) + self.props.visible = self.model.props.n_items + + +model = Gtk.SortListModel.new( + sources.model, + Gtk.StringSorter.new(Gtk.PropertyExpression.new(Source, None, "name")), +) diff --git a/cartridges/ui/window.blp b/cartridges/ui/window.blp index 037aa32..a872c7b 100644 --- a/cartridges/ui/window.blp +++ b/cartridges/ui/window.blp @@ -94,6 +94,10 @@ template $Window: Adw.ApplicationWindow { } } + Adw.SidebarSection sources { + title: _("Sources"); + } + Adw.SidebarSection collections { title: _("Collections"); diff --git a/cartridges/ui/window.py b/cartridges/ui/window.py index 08c2515..3df39ed 100644 --- a/cartridges/ui/window.py +++ b/cartridges/ui/window.py @@ -13,14 +13,15 @@ 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 imported -from cartridges.ui import collections, games +from cartridges.sources import Source, imported +from cartridges.ui import collections, games, sources from .collection_details import CollectionDetails from .collections import CollectionFilter, CollectionSidebarItem from .game_details import GameDetails from .game_item import GameItem # noqa: F401 from .games import GameSorter +from .sources import SourceSidebarItem if sys.platform.startswith("linux"): from cartridges import gamepads @@ -45,6 +46,7 @@ class Window(Adw.ApplicationWindow): split_view: Adw.OverlaySplitView = Gtk.Template.Child() sidebar: Adw.Sidebar = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue] + sources: Adw.SidebarSection = 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() @@ -108,6 +110,7 @@ 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.collections.bind_model( collections.model, lambda collection: CollectionSidebarItem(collection=collection), @@ -162,6 +165,18 @@ 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 _source_empty(self): + self.model = games.model + self.sidebar.props.selected = 0 + @Gtk.Template.Callback() def _show_sidebar_title(self, _obj, layout: str) -> bool: right_window_controls = layout.replace("appmenu", "").startswith(":") @@ -175,10 +190,15 @@ class Window(Adw.ApplicationWindow): case self.new_collection_item: self._add_collection() sidebar.props.selected = self._selected_sidebar_item + case SourceSidebarItem(): + self.collection = None + self.model = item.model case CollectionSidebarItem(): self.collection = item.collection + self.model = games.model case _: self.collection = None + self.model = games.model if item is not self.new_collection_item: self._selected_sidebar_item = index diff --git a/data/icons/heroic-symbolic.svg b/data/icons/heroic-symbolic.svg new file mode 100644 index 0000000..a50b119 --- /dev/null +++ b/data/icons/heroic-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/icons.gresource.xml.in b/data/icons/icons.gresource.xml.in index ac3aa52..fa0ef95 100644 --- a/data/icons/icons.gresource.xml.in +++ b/data/icons/icons.gresource.xml.in @@ -4,6 +4,10 @@ apply-symbolic.svg cancel-symbolic.svg filter-symbolic.svg + + heroic-symbolic.svg + imported-symbolic.svg + steam-symbolic.svg collection-symbolic.svg ball-symbolic.svg diff --git a/data/icons/imported-symbolic.svg b/data/icons/imported-symbolic.svg new file mode 100644 index 0000000..3162223 --- /dev/null +++ b/data/icons/imported-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/steam-symbolic.svg b/data/icons/steam-symbolic.svg new file mode 100644 index 0000000..9826a3b --- /dev/null +++ b/data/icons/steam-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/po/POTFILES.in b/po/POTFILES.in index c877ff6..661a261 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -17,6 +17,7 @@ cartridges/ui/game_details.py cartridges/ui/game_item.py cartridges/ui/games.py cartridges/ui/shortcuts-dialog.blp +cartridges/ui/sources.py cartridges/ui/window.blp cartridges/ui/window.py data/page.kramo.Cartridges.desktop.in