Compare commits

...

8 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
Jamie Gravendeel
c5cfa476ff sources: Add initial UI 2026-01-05 20:34:32 +01:00
Jamie Gravendeel
7bc9d6aee9 window: Move game filters to ui.games 2026-01-05 20:34:32 +01:00
Jamie Gravendeel
00795b83fd sources: Fix icon name 2026-01-05 20:34:32 +01:00
Jamie Gravendeel
f9cb794394 cover: Add width and height properties 2026-01-05 19:43:38 +01:00
Jamie Gravendeel
1aee234cbf cartridges: Use generic methods in favor of TypeVar 2026-01-05 19:34:02 +01:00
Jamie Gravendeel
21588fe92b application: Only use GTK after startup 2026-01-05 19:31:11 +01:00
20 changed files with 211 additions and 105 deletions

View File

@@ -19,6 +19,11 @@ class Application(Adw.Application):
def __init__(self): def __init__(self):
super().__init__(application_id=APP_ID) super().__init__(application_id=APP_ID)
@override
def do_startup(self):
Adw.Application.do_startup(self)
self.props.style_manager.props.color_scheme = Adw.ColorScheme.PREFER_DARK
self.add_action_entries(( self.add_action_entries((
("quit", lambda *_: self.quit()), ("quit", lambda *_: self.quit()),
("about", lambda *_: self._present_about_dialog()), ("about", lambda *_: self._present_about_dialog()),
@@ -28,11 +33,6 @@ class Application(Adw.Application):
sources.load() sources.load()
collections.load() collections.load()
@override
def do_startup(self):
Adw.Application.do_startup(self)
Adw.StyleManager.get_default().props.color_scheme = Adw.ColorScheme.PREFER_DARK
@override @override
def do_activate(self): def do_activate(self):
window = self.props.active_window or Window(application=self) window = self.props.active_window or Window(application=self)

View File

@@ -54,6 +54,8 @@ class _SourceModule(Protocol):
class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatibleMethodOverride] class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatibleMethodOverride]
"""A source of games to import.""" """A source of games to import."""
__gtype_name__ = __qualname__
id = GObject.Property(type=str) id = GObject.Property(type=str)
name = GObject.Property(type=str) name = GObject.Property(type=str)
icon_name = GObject.Property(type=str) icon_name = GObject.Property(type=str)
@@ -65,11 +67,11 @@ class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatib
self.id, self.name, self._module = module.ID, module.NAME, module self.id, self.name, self._module = module.ID, module.NAME, module
self.bind_property( self.bind_property(
"name", "id",
self, self,
"icon-name", "icon-name",
GObject.BindingFlags.SYNC_CREATE, GObject.BindingFlags.SYNC_CREATE,
lambda _, name: f"{name}-symbolic", lambda _, ident: f"{ident}-symbolic",
) )
try: try:

View File

@@ -1,5 +1,11 @@
using Gtk 4.0; using Gtk 4.0;
using Adw 1; using Adw 1;
using GObject 2.0;
GObject.SignalGroup collection_signals {
target: bind template.collection;
target-type: typeof<$Collection>;
}
template $CollectionDetails: Adw.Dialog { template $CollectionDetails: Adw.Dialog {
title: bind $_or(template.collection as <$Collection>.name, _("New Collection")) as <string>; title: bind $_or(template.collection as <$Collection>.name, _("New Collection")) as <string>;

View File

@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from itertools import product from itertools import product
from typing import Any, NamedTuple, TypeVar, cast from typing import Any, NamedTuple
from gi.repository import Adw, Gio, GObject, Gtk from gi.repository import Adw, Gio, GObject, Gtk
@@ -40,8 +40,6 @@ _ICONS = (
_Icon("fist", ""), _Icon("fist", ""),
) )
_T = TypeVar("_T")
@Gtk.Template.from_resource(f"{PREFIX}/collection-details.ui") @Gtk.Template.from_resource(f"{PREFIX}/collection-details.ui")
class CollectionDetails(Adw.Dialog): class CollectionDetails(Adw.Dialog):
@@ -52,6 +50,7 @@ class CollectionDetails(Adw.Dialog):
name_entry: Adw.EntryRow = Gtk.Template.Child() name_entry: Adw.EntryRow = Gtk.Template.Child()
icons_grid: Gtk.Grid = Gtk.Template.Child() icons_grid: Gtk.Grid = Gtk.Template.Child()
collection_signals: GObject.SignalGroup = Gtk.Template.Child()
sort_changed = GObject.Signal() sort_changed = GObject.Signal()
_selected_icon: str _selected_icon: str
@@ -65,10 +64,8 @@ class CollectionDetails(Adw.Dialog):
def collection(self, collection: Collection): def collection(self, collection: Collection):
self._collection = collection self._collection = collection
self.insert_action_group("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) super().__init__(**kwargs)
self.insert_action_group("details", group := Gio.SimpleActionGroup()) self.insert_action_group("details", group := Gio.SimpleActionGroup())
@@ -83,6 +80,13 @@ class CollectionDetails(Adw.Dialog):
transform_to=lambda _, text: bool(text), 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 group_button = None
for index, (row, col) in enumerate(product(range(3), range(7))): for index, (row, col) in enumerate(product(range(3), range(7))):
icon = _ICONS[index].name icon = _ICONS[index].name
@@ -132,9 +136,9 @@ class CollectionDetails(Adw.Dialog):
self.close() self.close()
@Gtk.Template.Callback() @Gtk.Template.Callback()
def _or(self, _obj, first: _T, second: _T) -> _T: def _or[T](self, _obj, first: T, second: T) -> T:
return first or second return first or second
@Gtk.Template.Callback() @Gtk.Template.Callback()
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T: def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return first if condition else second return first if condition else second

View File

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

View File

@@ -5,27 +5,27 @@ template $Cover: Adw.Bin {
child: Adw.Clamp { child: Adw.Clamp {
orientation: vertical; orientation: vertical;
unit: px; unit: px;
maximum-size: bind _picture.height-request; maximum-size: bind template.height;
tightening-threshold: bind _picture.height-request; tightening-threshold: bind template.height;
child: Adw.Clamp { child: Adw.Clamp {
unit: px; unit: px;
maximum-size: bind _picture.width-request; maximum-size: bind template.width;
tightening-threshold: bind _picture.width-request; tightening-threshold: bind template.width;
child: Adw.ViewStack { child: Adw.ViewStack {
name: "cover"; name: "cover";
visible-child-name: bind $_get_stack_child(template.paintable) as <string>; visible-child-name: bind $_if_else(template.paintable, "cover", "icon") as <string>;
overflow: hidden; overflow: hidden;
enable-transitions: true; enable-transitions: true;
Adw.ViewStackPage { Adw.ViewStackPage {
name: "cover"; name: "cover";
child: Picture _picture { child: Picture {
paintable: bind template.paintable; paintable: bind template.paintable;
width-request: 200; width-request: bind template.width;
height-request: 300; height-request: bind template.height;
content-fit: cover; content-fit: cover;
}; };
} }
@@ -34,7 +34,10 @@ template $Cover: Adw.Bin {
name: "icon"; name: "icon";
child: Image { child: Image {
icon-name: bind template.app-icon-name; icon-name: bind $_concat(
template.root as <Window>.application as <Application>.application-id,
"-symbolic"
) as <string>;
pixel-size: 80; pixel-size: 80;
styles [ styles [

View File

@@ -3,7 +3,7 @@
from gi.repository import Adw, Gdk, GObject, Gtk from gi.repository import Adw, Gdk, GObject, Gtk
from cartridges.config import APP_ID, PREFIX from cartridges.config import PREFIX
@Gtk.Template.from_resource(f"{PREFIX}/cover.ui") @Gtk.Template.from_resource(f"{PREFIX}/cover.ui")
@@ -12,12 +12,14 @@ class Cover(Adw.Bin):
__gtype_name__ = __qualname__ __gtype_name__ = __qualname__
picture = GObject.Property(lambda self: self._picture, type=Gtk.Picture)
paintable = GObject.Property(type=Gdk.Paintable) paintable = GObject.Property(type=Gdk.Paintable)
app_icon_name = GObject.Property(type=str, default=f"{APP_ID}-symbolic") width = GObject.Property(type=int, default=200)
height = GObject.Property(type=int, default=300)
_picture = Gtk.Template.Child()
@Gtk.Template.Callback() @Gtk.Template.Callback()
def _get_stack_child(self, _obj, paintable: Gdk.Paintable | None) -> str: def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return "cover" if paintable else "icon" return first if condition else second
@Gtk.Template.Callback()
def _concat(self, _obj, *strings: str) -> str:
return "".join(strings)

View File

@@ -1,6 +1,12 @@
using Gtk 4.0; using Gtk 4.0;
using Gdk 4.0; using Gdk 4.0;
using Adw 1; using Adw 1;
using GObject 2.0;
GObject.SignalGroup game_signals {
target: bind template.game;
target-type: typeof<$Game>;
}
template $GameDetails: Adw.NavigationPage { template $GameDetails: Adw.NavigationPage {
name: "details"; name: "details";

View File

@@ -12,8 +12,8 @@ template $GameItem: Box {
Adw.Clamp { Adw.Clamp {
unit: px; unit: px;
maximum-size: bind cover.picture as <Picture>.width-request; maximum-size: bind cover.width;
tightening-threshold: bind cover.picture as <Picture>.width-request; tightening-threshold: bind cover.width;
child: Overlay { child: Overlay {
child: $Cover cover { child: $Cover cover {

View File

@@ -7,7 +7,7 @@ import sys
import time import time
from datetime import UTC, datetime from datetime import UTC, datetime
from gettext import gettext as _ from gettext import gettext as _
from typing import Any, TypeVar, cast from typing import Any, cast
from urllib.parse import quote from urllib.parse import quote
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
@@ -20,14 +20,12 @@ from cartridges.sources import imported
from .collections import CollectionsBox from .collections import CollectionsBox
from .cover import Cover # noqa: F401 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} _EDITABLE_PROPERTIES = {prop.name for prop in games.PROPERTIES if prop.editable}
_REQUIRED_PROPERTIES = { _REQUIRED_PROPERTIES = {
prop.name for prop in games.PROPERTIES if prop.editable and prop.required prop.name for prop in games.PROPERTIES if prop.editable and prop.required
} }
_T = TypeVar("_T")
@Gtk.Template.from_resource(f"{PREFIX}/game-details.ui") @Gtk.Template.from_resource(f"{PREFIX}/game-details.ui")
class GameDetails(Adw.NavigationPage): class GameDetails(Adw.NavigationPage):
@@ -42,6 +40,7 @@ class GameDetails(Adw.NavigationPage):
developer_entry: Adw.EntryRow = Gtk.Template.Child() developer_entry: Adw.EntryRow = Gtk.Template.Child()
executable_entry: Adw.EntryRow = Gtk.Template.Child() executable_entry: Adw.EntryRow = Gtk.Template.Child()
game_signals: GObject.SignalGroup = Gtk.Template.Child()
sort_changed = GObject.Signal() sort_changed = GObject.Signal()
@GObject.Property(type=Game) @GObject.Property(type=Game)
@@ -54,21 +53,9 @@ class GameDetails(Adw.NavigationPage):
self._game = game self._game = game
self.insert_action_group("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): def __init__(self, **kwargs: Any):
super().__init__(**kwargs) super().__init__(**kwargs)
self._signal_ids = dict[Gio.SimpleAction, int]()
self.insert_action_group("details", group := Gio.SimpleActionGroup()) self.insert_action_group("details", group := Gio.SimpleActionGroup())
group.add_action_entries(( group.add_action_entries((
("edit", lambda *_: self.edit()), ("edit", lambda *_: self.edit()),
@@ -102,6 +89,13 @@ class GameDetails(Adw.NavigationPage):
valid = Gtk.ClosureExpression.new(bool, lambda _, *values: all(values), entries) valid = Gtk.ClosureExpression.new(bool, lambda _, *values: all(values), entries)
valid.bind(apply, "enabled") 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): def edit(self):
"""Enter edit mode.""" """Enter edit mode."""
for prop in _EDITABLE_PROPERTIES: for prop in _EDITABLE_PROPERTIES:
@@ -148,11 +142,11 @@ class GameDetails(Adw.NavigationPage):
self.collections_box.finish() self.collections_box.finish()
@Gtk.Template.Callback() @Gtk.Template.Callback()
def _or(self, _obj, first: _T, second: _T) -> _T: def _or[T](self, _obj, first: T, second: T) -> T:
return first or second return first or second
@Gtk.Template.Callback() @Gtk.Template.Callback()
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T: def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return first if condition else second return first if condition else second
@Gtk.Template.Callback() @Gtk.Template.Callback()

View File

@@ -19,7 +19,24 @@ _SORT_MODES = {
"oldest": ("added", False), "oldest": ("added", False),
} }
model = Gtk.FlattenListModel.new(sources.model) filter_ = Gtk.EveryFilter()
filter_.append(
Gtk.BoolFilter(
expression=Gtk.PropertyExpression.new(Game, None, "removed"),
invert=True,
)
)
filter_.append(
Gtk.BoolFilter(
expression=Gtk.PropertyExpression.new(Game, None, "blacklisted"),
invert=True,
)
)
model = Gtk.FilterListModel(
model=Gtk.FlattenListModel.new(sources.model),
filter=filter_,
watch_items=True, # pyright: ignore[reportCallIssue]
)
class GameSorter(Gtk.Sorter): class GameSorter(Gtk.Sorter):

View File

@@ -7,6 +7,7 @@ python.install_sources(
'game_details.py', 'game_details.py',
'game_item.py', 'game_item.py',
'games.py', 'games.py',
'sources.py',
'window.py', 'window.py',
), ),
subdir: 'cartridges' / 'ui', subdir: 'cartridges' / 'ui',

50
cartridges/ui/sources.py Normal file
View File

@@ -0,0 +1,50 @@
# 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
from cartridges.sources import Source
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)
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, *_: model.notify("n-items"),
after=True,
)
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(
sources.model,
Gtk.StringSorter.new(Gtk.PropertyExpression.new(Source, None, "name")),
)

View File

@@ -1,5 +1,17 @@
using Gtk 4.0; using Gtk 4.0;
using Adw 1; 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 { template $Window: Adw.ApplicationWindow {
realize => $_setup_gamepad_monitor(); realize => $_setup_gamepad_monitor();
@@ -94,6 +106,10 @@ template $Window: Adw.ApplicationWindow {
} }
} }
Adw.SidebarSection sources {
title: _("Sources");
}
Adw.SidebarSection collections { Adw.SidebarSection collections {
title: _("Collections"); title: _("Collections");
@@ -262,16 +278,6 @@ template $Window: Adw.ApplicationWindow {
expression: expr item as <$Game>.hidden; expression: expr item as <$Game>.hidden;
invert: bind template.show-hidden inverted; invert: bind template.show-hidden inverted;
} }
BoolFilter {
expression: expr item as <$Game>.removed;
invert: true;
}
BoolFilter {
expression: expr item as <$Game>.blacklisted;
invert: true;
}
}; };
model: bind template.model; model: bind template.model;

View File

@@ -6,7 +6,7 @@
import sys import sys
from collections.abc import Callable from collections.abc import Callable
from gettext import gettext as _ from gettext import gettext as _
from typing import Any, TypeVar, cast from typing import Any, cast
from gi.repository import Adw, Gio, GLib, GObject, Gtk from gi.repository import Adw, Gio, GLib, GObject, Gtk
@@ -14,13 +14,14 @@ from cartridges import STATE_SETTINGS
from cartridges.collections import Collection from cartridges.collections import Collection
from cartridges.config import PREFIX, PROFILE from cartridges.config import PREFIX, PROFILE
from cartridges.sources import imported from cartridges.sources import imported
from cartridges.ui import collections, games from cartridges.ui import collections, games, sources
from .collection_details import CollectionDetails from .collection_details import CollectionDetails
from .collections import CollectionFilter, CollectionSidebarItem from .collections import CollectionFilter, CollectionSidebarItem
from .game_details import GameDetails from .game_details import GameDetails
from .game_item import GameItem # noqa: F401 from .game_item import GameItem # noqa: F401
from .games import GameSorter from .games import GameSorter
from .sources import SourceSidebarItem
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
from cartridges import gamepads from cartridges import gamepads
@@ -34,7 +35,6 @@ SORT_MODES = {
"oldest": ("added", True), "oldest": ("added", True),
} }
_T = TypeVar("_T")
type _UndoFunc = Callable[[], Any] type _UndoFunc = Callable[[], Any]
@@ -46,6 +46,7 @@ class Window(Adw.ApplicationWindow):
split_view: Adw.OverlaySplitView = Gtk.Template.Child() split_view: Adw.OverlaySplitView = Gtk.Template.Child()
sidebar: Adw.Sidebar = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue] 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] collections: Adw.SidebarSection = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
new_collection_item: Adw.SidebarItem = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue] new_collection_item: Adw.SidebarItem = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
collection_menu: Gio.Menu = Gtk.Template.Child() collection_menu: Gio.Menu = Gtk.Template.Child()
@@ -61,38 +62,18 @@ class Window(Adw.ApplicationWindow):
collection_filter: CollectionFilter = Gtk.Template.Child() collection_filter: CollectionFilter = Gtk.Template.Child()
details: GameDetails = 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) search_text = GObject.Property(type=str)
show_hidden = GObject.Property(type=bool, default=False) show_hidden = GObject.Property(type=bool, default=False)
settings = GObject.Property(type=Gtk.Settings) settings = GObject.Property(type=Gtk.Settings)
_collection: Collection | None = None
_collection_removed_signal: int | None = None
_selected_sidebar_item = 0 _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): def __init__(self, **kwargs: Any):
super().__init__(**kwargs) super().__init__(**kwargs)
@@ -109,9 +90,13 @@ class Window(Adw.ApplicationWindow):
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901 # https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
self.search_entry.set_key_capture_widget(self) self.search_entry.set_key_capture_widget(self)
self.sources.bind_model(
sources.model,
lambda source: SourceSidebarItem(source),
)
self.collections.bind_model( self.collections.bind_model(
collections.model, collections.model,
lambda collection: CollectionSidebarItem(collection=collection), lambda collection: CollectionSidebarItem(collection),
) )
self.add_action(STATE_SETTINGS.create_action("show-sidebar")) self.add_action(STATE_SETTINGS.create_action("show-sidebar"))
@@ -147,6 +132,18 @@ class Window(Adw.ApplicationWindow):
("undo", lambda *_: self._undo()), ("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] = {} self._history: dict[Adw.Toast, _UndoFunc] = {}
def send_toast(self, title: str, *, undo: _UndoFunc | None = None): def send_toast(self, title: str, *, undo: _UndoFunc | None = None):
@@ -163,6 +160,14 @@ class Window(Adw.ApplicationWindow):
self.toast_overlay.add_toast(toast) self.toast_overlay.add_toast(toast)
def _collection_removed(self):
self.collection = None
self.sidebar.props.selected = 0
def _model_emptied(self):
self.model = games.model
self.sidebar.props.selected = 0
@Gtk.Template.Callback() @Gtk.Template.Callback()
def _show_sidebar_title(self, _obj, layout: str) -> bool: def _show_sidebar_title(self, _obj, layout: str) -> bool:
right_window_controls = layout.replace("appmenu", "").startswith(":") right_window_controls = layout.replace("appmenu", "").startswith(":")
@@ -176,10 +181,15 @@ class Window(Adw.ApplicationWindow):
case self.new_collection_item: case self.new_collection_item:
self._add_collection() self._add_collection()
sidebar.props.selected = self._selected_sidebar_item sidebar.props.selected = self._selected_sidebar_item
case SourceSidebarItem():
self.collection = None
self.model = item.model
case CollectionSidebarItem(): case CollectionSidebarItem():
self.collection = item.collection self.collection = item.collection
self.model = games.model
case _: case _:
self.collection = None self.collection = None
self.model = games.model
if item is not self.new_collection_item: if item is not self.new_collection_item:
self._selected_sidebar_item = index self._selected_sidebar_item = index
@@ -214,7 +224,7 @@ class Window(Adw.ApplicationWindow):
gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable] gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable]
@Gtk.Template.Callback() @Gtk.Template.Callback()
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T: def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return first if condition else second return first if condition else second
@Gtk.Template.Callback() @Gtk.Template.Callback()
@@ -266,12 +276,12 @@ class Window(Adw.ApplicationWindow):
if game_id: if game_id:
collection.game_ids.add(game_id) collection.game_ids.add(game_id)
details = CollectionDetails(collection=collection) details = CollectionDetails(collection)
details.present(self) details.present(self)
def _edit_collection(self, pos: int): def _edit_collection(self, pos: int):
collection = self.collections.get_item(pos).collection collection = self.collections.get_item(pos).collection
details = CollectionDetails(collection=collection) details = CollectionDetails(collection)
details.connect( details.connect(
"sort-changed", "sort-changed",
lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT), lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT),

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="#000" fill-rule="evenodd" d="m7.872 16-3.817-3.083L2 2.79 7.872 0l5.871 2.789-2.055 10.128zm0-4.257-.294-.293-.88-7.927 1.1-1.908 1.174 1.908-.807 7.927zm-.294.367-.147.367-1.761.294-.294-.66.294-.662 1.761.294zm-.073.734-.22 1.541.587.294.587-.294-.22-1.541-.367-.22zm.807-.367-.147-.367.147-.367 1.761-.293.294.66-.294.66z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@@ -4,6 +4,10 @@
<file>apply-symbolic.svg</file> <file>apply-symbolic.svg</file>
<file>cancel-symbolic.svg</file> <file>cancel-symbolic.svg</file>
<file>filter-symbolic.svg</file> <file>filter-symbolic.svg</file>
<!-- Sources -->
<file>heroic-symbolic.svg</file>
<file>imported-symbolic.svg</file>
<file>steam-symbolic.svg</file>
<!-- Categories --> <!-- Categories -->
<file>collection-symbolic.svg</file> <file>collection-symbolic.svg</file>
<file>ball-symbolic.svg</file> <file>ball-symbolic.svg</file>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#222" d="M7 1v6H1v2h6v6h2V9h6V7H9V1zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 144 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g clip-path="url(#a)"><path fill="#000" fill-rule="evenodd" d="M9.352 5.1a1.509 1.509 0 1 0 2.51 1.675A1.509 1.509 0 0 0 9.352 5.1m2.923-.277a2.009 2.009 0 1 1-3.34 2.231 2.009 2.009 0 0 1 3.34-2.23ZM5.01 12.131l-.983-.407a1.7 1.7 0 0 0 3.108-.103 1.696 1.696 0 0 0-1.213-2.29 1.7 1.7 0 0 0-.966.07l1.015.421a1.249 1.249 0 0 1-.96 2.307zM2.546 2.121A8 8 0 0 1 7.966 0l.003.013a7.99 7.99 0 0 1 7.159 4.432 7.996 7.996 0 0 1-4.277 11.018 7.99 7.99 0 0 1-8.274-1.558A8 8 0 0 1 .279 10.18l3.064 1.267A2.264 2.264 0 0 0 7.823 11v-.107l2.718-1.938h.063A3.016 3.016 0 1 0 7.589 5.94v.031l-1.906 2.76h-.126c-.454 0-.898.138-1.273.395L0 7.354A8 8 0 0 1 2.546 2.12Z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 842 B

View File

@@ -17,6 +17,7 @@ cartridges/ui/game_details.py
cartridges/ui/game_item.py cartridges/ui/game_item.py
cartridges/ui/games.py cartridges/ui/games.py
cartridges/ui/shortcuts-dialog.blp cartridges/ui/shortcuts-dialog.blp
cartridges/ui/sources.py
cartridges/ui/window.blp cartridges/ui/window.blp
cartridges/ui/window.py cartridges/ui/window.py
data/page.kramo.Cartridges.desktop.in data/page.kramo.Cartridges.desktop.in