Compare commits
1 Commits
rewrite-si
...
rewrite-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94830393b4 |
46
cartridges/ui/closures.py
Normal file
46
cartridges/ui/closures.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
|
||||||
|
def _closure[**P, R](func: Callable[P, R]) -> object: # gi._gtktemplate.CallThing
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
@staticmethod
|
||||||
|
def wrapper(_obj, *args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@_closure
|
||||||
|
def boolean(value: object) -> bool:
|
||||||
|
"""Get a boolean for `value`."""
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
@_closure
|
||||||
|
def concat(*strings: str) -> str:
|
||||||
|
"""Join `strings`."""
|
||||||
|
return "".join(strings)
|
||||||
|
|
||||||
|
|
||||||
|
@_closure
|
||||||
|
def either[T](first: T, second: T) -> T:
|
||||||
|
"""Return `first` or `second`."""
|
||||||
|
return first or second
|
||||||
|
|
||||||
|
|
||||||
|
@_closure
|
||||||
|
def format_string(string: str, *args: Any) -> str:
|
||||||
|
"""Format `string` with `args`."""
|
||||||
|
return string.format(*args)
|
||||||
|
|
||||||
|
|
||||||
|
@_closure
|
||||||
|
def if_else[T](condition: object, first: T, second: T) -> T:
|
||||||
|
"""Return `first` or `second` depending on `condition`."""
|
||||||
|
return first if condition else second
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
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 $either(template.collection as <$Collection>.name, _("New Collection")) as <string>;
|
||||||
content-width: 360;
|
content-width: 360;
|
||||||
default-widget: apply_button;
|
default-widget: apply_button;
|
||||||
focus-widget: name_entry;
|
focus-widget: name_entry;
|
||||||
@@ -37,7 +31,7 @@ template $CollectionDetails: Adw.Dialog {
|
|||||||
Button apply_button {
|
Button apply_button {
|
||||||
action-name: "details.apply";
|
action-name: "details.apply";
|
||||||
icon-name: "apply-symbolic";
|
icon-name: "apply-symbolic";
|
||||||
tooltip-text: bind $_if_else(template.collection as <$Collection>.in-model, _("Apply"), _("Add")) as <string>;
|
tooltip-text: bind $if_else(template.collection as <$Collection>.in-model, _("Apply"), _("Add")) as <string>;
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
"suggested-action",
|
"suggested-action",
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
# 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
|
from typing import Any, NamedTuple, cast
|
||||||
|
|
||||||
from gi.repository import Adw, Gio, GObject, Gtk
|
from gi.repository import Adw, Gio, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import collections
|
from cartridges import collections
|
||||||
from cartridges.collections import Collection
|
from cartridges.collections import Collection
|
||||||
from cartridges.config import PREFIX
|
from cartridges.config import PREFIX
|
||||||
|
from cartridges.ui import closures
|
||||||
|
|
||||||
|
|
||||||
class _Icon(NamedTuple):
|
class _Icon(NamedTuple):
|
||||||
@@ -50,11 +51,13 @@ 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
|
||||||
|
|
||||||
|
either = closures.either
|
||||||
|
if_else = closures.if_else
|
||||||
|
|
||||||
@GObject.Property(type=Collection)
|
@GObject.Property(type=Collection)
|
||||||
def collection(self) -> Collection:
|
def collection(self) -> Collection:
|
||||||
"""The collection that `self` represents."""
|
"""The collection that `self` represents."""
|
||||||
@@ -64,8 +67,10 @@ 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, collection: Collection, **kwargs: Any):
|
def __init__(self, **kwargs: Any):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.insert_action_group("details", group := Gio.SimpleActionGroup())
|
self.insert_action_group("details", group := Gio.SimpleActionGroup())
|
||||||
@@ -80,13 +85,6 @@ 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
|
||||||
@@ -134,11 +132,3 @@ class CollectionDetails(Adw.Dialog):
|
|||||||
|
|
||||||
collections.save()
|
collections.save()
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _or[T](self, _obj, first: T, second: T) -> T:
|
|
||||||
return first or second
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
|
|
||||||
return first if condition else second
|
|
||||||
|
|||||||
@@ -37,9 +37,19 @@ 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."""
|
||||||
|
|
||||||
collection = GObject.Property(type=Collection)
|
@GObject.Property(type=Collection)
|
||||||
|
def collection(self) -> Collection:
|
||||||
|
"""The collection that `self` represents."""
|
||||||
|
return self._collection
|
||||||
|
|
||||||
def __init__(self, collection: Collection, **kwargs: Any):
|
@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):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.bind_property(
|
self.bind_property(
|
||||||
@@ -50,13 +60,6 @@ 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."""
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ template $Cover: Adw.Bin {
|
|||||||
|
|
||||||
child: Adw.ViewStack {
|
child: Adw.ViewStack {
|
||||||
name: "cover";
|
name: "cover";
|
||||||
visible-child-name: bind $_if_else(template.paintable, "cover", "icon") as <string>;
|
visible-child-name: bind $if_else(template.paintable, "cover", "icon") as <string>;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
enable-transitions: true;
|
enable-transitions: true;
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ template $Cover: Adw.Bin {
|
|||||||
name: "icon";
|
name: "icon";
|
||||||
|
|
||||||
child: Image {
|
child: Image {
|
||||||
icon-name: bind $_concat(
|
icon-name: bind $concat(
|
||||||
template.root as <Window>.application as <Application>.application-id,
|
template.root as <Window>.application as <Application>.application-id,
|
||||||
"-symbolic"
|
"-symbolic"
|
||||||
) as <string>;
|
) as <string>;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from gi.repository import Adw, Gdk, GObject, Gtk
|
from gi.repository import Adw, Gdk, GObject, Gtk
|
||||||
|
|
||||||
from cartridges.config import PREFIX
|
from cartridges.config import PREFIX
|
||||||
|
from cartridges.ui import closures
|
||||||
|
|
||||||
|
|
||||||
@Gtk.Template.from_resource(f"{PREFIX}/cover.ui")
|
@Gtk.Template.from_resource(f"{PREFIX}/cover.ui")
|
||||||
@@ -16,10 +17,5 @@ class Cover(Adw.Bin):
|
|||||||
width = GObject.Property(type=int, default=200)
|
width = GObject.Property(type=int, default=200)
|
||||||
height = GObject.Property(type=int, default=300)
|
height = GObject.Property(type=int, default=300)
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
concat = closures.concat
|
||||||
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
|
if_else = closures.if_else
|
||||||
return first if condition else second
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _concat(self, _obj, *strings: str) -> str:
|
|
||||||
return "".join(strings)
|
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
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";
|
||||||
tag: "details";
|
tag: "details";
|
||||||
title: bind $_or(template.game as <$Game>.name, _("Add Game")) as <string>;
|
title: bind $either(template.game as <$Game>.name, _("Add Game")) as <string>;
|
||||||
hidden => $_cancel();
|
hidden => $_cancel();
|
||||||
|
|
||||||
ShortcutController {
|
ShortcutController {
|
||||||
@@ -107,7 +101,7 @@ template $GameDetails: Adw.NavigationPage {
|
|||||||
|
|
||||||
Label developer_label {
|
Label developer_label {
|
||||||
label: bind template.game as <$Game>.developer;
|
label: bind template.game as <$Game>.developer;
|
||||||
visible: bind $_bool(template.game as <$Game>.developer) as <bool>;
|
visible: bind $boolean(template.game as <$Game>.developer) as <bool>;
|
||||||
halign: start;
|
halign: start;
|
||||||
max-width-chars: 36;
|
max-width-chars: 36;
|
||||||
wrap: true;
|
wrap: true;
|
||||||
@@ -125,14 +119,20 @@ template $GameDetails: Adw.NavigationPage {
|
|||||||
spacing: 9;
|
spacing: 9;
|
||||||
|
|
||||||
Label last_played_label {
|
Label last_played_label {
|
||||||
label: bind $_date_label(_("Last played: {}"), template.game as <$Game>.last_played) as <string>;
|
label: bind $format_string(
|
||||||
|
_("Last played: {}"),
|
||||||
|
$_pretty_date(template.game as <$Game>.last_played) as <string>
|
||||||
|
) as <string>;
|
||||||
halign: start;
|
halign: start;
|
||||||
wrap: true;
|
wrap: true;
|
||||||
wrap-mode: word_char;
|
wrap-mode: word_char;
|
||||||
}
|
}
|
||||||
|
|
||||||
Label added_label {
|
Label added_label {
|
||||||
label: bind $_date_label(_("Added: {}"), template.game as <$Game>.added) as <string>;
|
label: bind $format_string(
|
||||||
|
_("Added: {}"),
|
||||||
|
$_pretty_date(template.game as <$Game>.added) as <string>
|
||||||
|
) as <string>;
|
||||||
halign: start;
|
halign: start;
|
||||||
wrap: true;
|
wrap: true;
|
||||||
wrap-mode: word_char;
|
wrap-mode: word_char;
|
||||||
@@ -382,7 +382,7 @@ template $GameDetails: Adw.NavigationPage {
|
|||||||
|
|
||||||
Button apply_button {
|
Button apply_button {
|
||||||
action-name: "details.apply";
|
action-name: "details.apply";
|
||||||
label: bind $_if_else(template.game as <$Game>.added, _("Apply"), _("Add")) as <string>;
|
label: bind $if_else(template.game as <$Game>.added, _("Apply"), _("Add")) as <string>;
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
"pill",
|
"pill",
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ from cartridges import games, sources
|
|||||||
from cartridges.config import PREFIX
|
from cartridges.config import PREFIX
|
||||||
from cartridges.games import Game
|
from cartridges.games import Game
|
||||||
from cartridges.sources import imported
|
from cartridges.sources import imported
|
||||||
|
from cartridges.ui import closures
|
||||||
|
|
||||||
from .collections import CollectionsBox
|
from .collections import CollectionsBox
|
||||||
from .cover import Cover # noqa: F401
|
from .cover import Cover # noqa: F401
|
||||||
|
|
||||||
_POP_ON_PROPERTY_NOTIFY = "hidden", "removed"
|
_POP_ON_ACTION = "hide", "unhide", "remove"
|
||||||
_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
|
||||||
@@ -40,9 +41,13 @@ 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()
|
||||||
|
|
||||||
|
boolean = closures.boolean
|
||||||
|
either = closures.either
|
||||||
|
format_string = closures.format_string
|
||||||
|
if_else = closures.if_else
|
||||||
|
|
||||||
@GObject.Property(type=Game)
|
@GObject.Property(type=Game)
|
||||||
def game(self) -> Game:
|
def game(self) -> Game:
|
||||||
"""The game whose details to show."""
|
"""The game whose details to show."""
|
||||||
@@ -53,9 +58,21 @@ 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()),
|
||||||
@@ -89,13 +106,6 @@ 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:
|
||||||
@@ -141,14 +151,6 @@ class GameDetails(Adw.NavigationPage):
|
|||||||
else:
|
else:
|
||||||
self.collections_box.finish()
|
self.collections_box.finish()
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _or[T](self, _obj, first: T, second: T) -> T:
|
|
||||||
return first or second
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
|
|
||||||
return first if condition else second
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None:
|
def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None:
|
||||||
if cover and (renderer := cast(Gtk.Native, self.props.root).get_renderer()):
|
if cover and (renderer := cast(Gtk.Native, self.props.root).get_renderer()):
|
||||||
@@ -159,10 +161,10 @@ class GameDetails(Adw.NavigationPage):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _date_label(self, _obj, label: str, timestamp: int) -> str:
|
def _pretty_date(self, _obj, timestamp: int) -> str:
|
||||||
date = datetime.fromtimestamp(timestamp, UTC)
|
date = datetime.fromtimestamp(timestamp, UTC)
|
||||||
now = datetime.now(UTC)
|
now = datetime.now(UTC)
|
||||||
return label.format(
|
return (
|
||||||
_("Never")
|
_("Never")
|
||||||
if not timestamp
|
if not timestamp
|
||||||
else _("Today")
|
else _("Today")
|
||||||
@@ -184,10 +186,6 @@ class GameDetails(Adw.NavigationPage):
|
|||||||
else date.strftime("%Y")
|
else date.strftime("%Y")
|
||||||
)
|
)
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _bool(self, _obj, o: object) -> bool:
|
|
||||||
return bool(o)
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _format_more_info(self, _obj, label: str) -> str:
|
def _format_more_info(self, _obj, label: str) -> str:
|
||||||
executable = _("program")
|
executable = _("program")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
python.install_sources(
|
python.install_sources(
|
||||||
files(
|
files(
|
||||||
'__init__.py',
|
'__init__.py',
|
||||||
|
'closures.py',
|
||||||
'collection_details.py',
|
'collection_details.py',
|
||||||
'collections.py',
|
'collections.py',
|
||||||
'cover.py',
|
'cover.py',
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from gi.repository import Adw, Gio, GObject, Gtk
|
from gi.repository import Adw, Gio, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import sources
|
from cartridges import sources
|
||||||
@@ -13,35 +11,31 @@ from cartridges.ui import games
|
|||||||
class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
|
class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
|
||||||
"""A sidebar item representing a source."""
|
"""A sidebar item representing a source."""
|
||||||
|
|
||||||
source = GObject.Property(type=Source)
|
|
||||||
model = GObject.Property(type=Gio.ListModel)
|
model = GObject.Property(type=Gio.ListModel)
|
||||||
|
|
||||||
def __init__(self, source: Source, **kwargs: Any):
|
@GObject.Property(type=Source)
|
||||||
super().__init__(**kwargs)
|
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
|
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7959
|
||||||
self._model_signals = GObject.SignalGroup.new(Gio.ListModel)
|
self.model.connect(
|
||||||
self._model_signals.connect_closure(
|
|
||||||
"items-changed",
|
"items-changed",
|
||||||
lambda model, *_: model.notify("n-items"),
|
lambda *_: self.set_property("visible", self.model.props.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(
|
model = Gtk.SortListModel.new(
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
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();
|
||||||
@@ -225,16 +213,16 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
|
|
||||||
content: Adw.ToastOverlay toast_overlay {
|
content: Adw.ToastOverlay toast_overlay {
|
||||||
child: Adw.ViewStack {
|
child: Adw.ViewStack {
|
||||||
visible-child-name: bind $_if_else(
|
visible-child-name: bind $if_else(
|
||||||
grid.model as <NoSelection>.n-items,
|
grid.model as <NoSelection>.n-items,
|
||||||
"grid",
|
"grid",
|
||||||
$_if_else(
|
$if_else(
|
||||||
template.search-text,
|
template.search-text,
|
||||||
"empty-search",
|
"empty-search",
|
||||||
$_if_else(
|
$if_else(
|
||||||
template.collection,
|
template.collection,
|
||||||
"empty-collection",
|
"empty-collection",
|
||||||
$_if_else(template.show-hidden, "empty-hidden", "empty") as <string>
|
$if_else(template.show-hidden, "empty-hidden", "empty") as <string>
|
||||||
) as <string>
|
) as <string>
|
||||||
) as <string>
|
) as <string>
|
||||||
) as <string>;
|
) as <string>;
|
||||||
@@ -322,7 +310,10 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
|
|
||||||
child: Adw.StatusPage {
|
child: Adw.StatusPage {
|
||||||
icon-name: bind template.collection as <$Collection>.icon-name;
|
icon-name: bind template.collection as <$Collection>.icon-name;
|
||||||
title: bind $_format(_("No Games in {}"), template.collection as <$Collection>.name) as <string>;
|
title: bind $format_string(
|
||||||
|
_("No Games in {}"),
|
||||||
|
template.collection as <$Collection>.name,
|
||||||
|
) as <string>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
|||||||
from cartridges import STATE_SETTINGS
|
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 Source, imported
|
||||||
from cartridges.ui import collections, games, sources
|
from cartridges.ui import closures, collections, games, sources
|
||||||
|
|
||||||
from .collection_details import CollectionDetails
|
from .collection_details import CollectionDetails
|
||||||
from .collections import CollectionFilter, CollectionSidebarItem
|
from .collections import CollectionFilter, CollectionSidebarItem
|
||||||
@@ -62,18 +62,41 @@ 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()
|
||||||
|
|
||||||
collection = GObject.Property(type=Collection)
|
model = GObject.Property(type=Gio.ListModel, default=games.model)
|
||||||
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
|
||||||
|
|
||||||
|
format_string = closures.format_string
|
||||||
|
if_else = closures.if_else
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
@@ -90,13 +113,10 @@ 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(
|
self.sources.bind_model(sources.model, self._create_source_item)
|
||||||
sources.model,
|
|
||||||
lambda source: SourceSidebarItem(source),
|
|
||||||
)
|
|
||||||
self.collections.bind_model(
|
self.collections.bind_model(
|
||||||
collections.model,
|
collections.model,
|
||||||
lambda collection: CollectionSidebarItem(collection),
|
lambda collection: CollectionSidebarItem(collection=collection),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.add_action(STATE_SETTINGS.create_action("show-sidebar"))
|
self.add_action(STATE_SETTINGS.create_action("show-sidebar"))
|
||||||
@@ -132,18 +152,6 @@ 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):
|
||||||
@@ -160,11 +168,15 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
self.toast_overlay.add_toast(toast)
|
self.toast_overlay.add_toast(toast)
|
||||||
|
|
||||||
def _collection_removed(self):
|
def _create_source_item(self, source: Source) -> SourceSidebarItem:
|
||||||
self.collection = None
|
item = SourceSidebarItem(source=source)
|
||||||
self.sidebar.props.selected = 0
|
item.connect(
|
||||||
|
"notify::visible",
|
||||||
|
lambda item, _: self._source_empty() if not item.props.visible else None,
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
def _model_emptied(self):
|
def _source_empty(self):
|
||||||
self.model = games.model
|
self.model = games.model
|
||||||
self.sidebar.props.selected = 0
|
self.sidebar.props.selected = 0
|
||||||
|
|
||||||
@@ -223,14 +235,6 @@ class Window(Adw.ApplicationWindow):
|
|||||||
Gamepad.window = self # pyright: ignore[reportPossiblyUnboundVariable]
|
Gamepad.window = self # pyright: ignore[reportPossiblyUnboundVariable]
|
||||||
gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable]
|
gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable]
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
|
|
||||||
return first if condition else second
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _format(self, _obj, string: str, *args: Any) -> str:
|
|
||||||
return string.format(*args)
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _show_details(self, grid: Gtk.GridView, position: int):
|
def _show_details(self, grid: Gtk.GridView, position: int):
|
||||||
self.details.game = cast(Gio.ListModel, grid.props.model).get_item(position)
|
self.details.game = cast(Gio.ListModel, grid.props.model).get_item(position)
|
||||||
@@ -276,12 +280,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)
|
details = CollectionDetails(collection=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)
|
details = CollectionDetails(collection=collection)
|
||||||
details.connect(
|
details.connect(
|
||||||
"sort-changed",
|
"sort-changed",
|
||||||
lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT),
|
lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ cartridges/sources/__init__.py
|
|||||||
cartridges/sources/heroic.py
|
cartridges/sources/heroic.py
|
||||||
cartridges/sources/imported.py
|
cartridges/sources/imported.py
|
||||||
cartridges/sources/steam.py
|
cartridges/sources/steam.py
|
||||||
|
cartridges/ui/closures.py
|
||||||
cartridges/ui/collection-details.blp
|
cartridges/ui/collection-details.blp
|
||||||
cartridges/ui/collection_details.py
|
cartridges/ui/collection_details.py
|
||||||
cartridges/ui/collections.py
|
cartridges/ui/collections.py
|
||||||
|
|||||||
Reference in New Issue
Block a user