Compare commits

...

1 Commits

Author SHA1 Message Date
Jamie Gravendeel
94830393b4 closures: Add a module for commonly used closures 2026-01-10 01:38:55 +01:00
11 changed files with 90 additions and 52 deletions

46
cartridges/ui/closures.py Normal file
View 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

View File

@@ -2,7 +2,7 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
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;
@@ -31,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",

View File

@@ -9,6 +9,7 @@ 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):
@@ -54,6 +55,9 @@ class CollectionDetails(Adw.Dialog):
_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."""
@@ -128,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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ using Adw 1;
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 {
@@ -101,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;
@@ -119,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;
@@ -376,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",

View File

@@ -16,6 +16,7 @@ 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
@@ -42,6 +43,11 @@ class GameDetails(Adw.NavigationPage):
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."""
@@ -145,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()):
@@ -163,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")
@@ -188,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")

View File

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

View File

@@ -213,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>;
@@ -310,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>;
}; };
} }

View File

@@ -14,7 +14,7 @@ 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 Source, 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
@@ -73,6 +73,9 @@ class Window(Adw.ApplicationWindow):
_collection_removed_signal: int | 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) @GObject.Property(type=Collection)
def collection(self) -> Collection | None: def collection(self) -> Collection | None:
"""The currently selected collection.""" """The currently selected collection."""
@@ -232,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)

View File

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