closures: Add a module for commonly used closures
This commit is contained in:
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
|
||||
@@ -2,7 +2,7 @@ using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
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;
|
||||
default-widget: apply_button;
|
||||
focus-widget: name_entry;
|
||||
@@ -31,7 +31,7 @@ template $CollectionDetails: Adw.Dialog {
|
||||
Button apply_button {
|
||||
action-name: "details.apply";
|
||||
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 [
|
||||
"suggested-action",
|
||||
|
||||
@@ -9,6 +9,7 @@ from gi.repository import Adw, Gio, GObject, Gtk
|
||||
from cartridges import collections
|
||||
from cartridges.collections import Collection
|
||||
from cartridges.config import PREFIX
|
||||
from cartridges.ui import closures
|
||||
|
||||
|
||||
class _Icon(NamedTuple):
|
||||
@@ -54,6 +55,9 @@ class CollectionDetails(Adw.Dialog):
|
||||
|
||||
_selected_icon: str
|
||||
|
||||
either = closures.either
|
||||
if_else = closures.if_else
|
||||
|
||||
@GObject.Property(type=Collection)
|
||||
def collection(self) -> Collection:
|
||||
"""The collection that `self` represents."""
|
||||
@@ -128,11 +132,3 @@ class CollectionDetails(Adw.Dialog):
|
||||
|
||||
collections.save()
|
||||
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
|
||||
|
||||
@@ -15,7 +15,7 @@ template $Cover: Adw.Bin {
|
||||
|
||||
child: Adw.ViewStack {
|
||||
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;
|
||||
enable-transitions: true;
|
||||
|
||||
@@ -34,7 +34,7 @@ template $Cover: Adw.Bin {
|
||||
name: "icon";
|
||||
|
||||
child: Image {
|
||||
icon-name: bind $_concat(
|
||||
icon-name: bind $concat(
|
||||
template.root as <Window>.application as <Application>.application-id,
|
||||
"-symbolic"
|
||||
) as <string>;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from gi.repository import Adw, Gdk, GObject, Gtk
|
||||
|
||||
from cartridges.config import PREFIX
|
||||
from cartridges.ui import closures
|
||||
|
||||
|
||||
@Gtk.Template.from_resource(f"{PREFIX}/cover.ui")
|
||||
@@ -16,10 +17,5 @@ class Cover(Adw.Bin):
|
||||
width = GObject.Property(type=int, default=200)
|
||||
height = GObject.Property(type=int, default=300)
|
||||
|
||||
@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 _concat(self, _obj, *strings: str) -> str:
|
||||
return "".join(strings)
|
||||
concat = closures.concat
|
||||
if_else = closures.if_else
|
||||
|
||||
@@ -5,7 +5,7 @@ using Adw 1;
|
||||
template $GameDetails: Adw.NavigationPage {
|
||||
name: "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();
|
||||
|
||||
ShortcutController {
|
||||
@@ -101,7 +101,7 @@ template $GameDetails: Adw.NavigationPage {
|
||||
|
||||
Label developer_label {
|
||||
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;
|
||||
max-width-chars: 36;
|
||||
wrap: true;
|
||||
@@ -119,14 +119,20 @@ template $GameDetails: Adw.NavigationPage {
|
||||
spacing: 9;
|
||||
|
||||
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;
|
||||
wrap: true;
|
||||
wrap-mode: word_char;
|
||||
}
|
||||
|
||||
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;
|
||||
wrap: true;
|
||||
wrap-mode: word_char;
|
||||
@@ -376,7 +382,7 @@ template $GameDetails: Adw.NavigationPage {
|
||||
|
||||
Button apply_button {
|
||||
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 [
|
||||
"pill",
|
||||
|
||||
@@ -16,6 +16,7 @@ from cartridges import games, sources
|
||||
from cartridges.config import PREFIX
|
||||
from cartridges.games import Game
|
||||
from cartridges.sources import imported
|
||||
from cartridges.ui import closures
|
||||
|
||||
from .collections import CollectionsBox
|
||||
from .cover import Cover # noqa: F401
|
||||
@@ -42,6 +43,11 @@ class GameDetails(Adw.NavigationPage):
|
||||
|
||||
sort_changed = GObject.Signal()
|
||||
|
||||
boolean = closures.boolean
|
||||
either = closures.either
|
||||
format_string = closures.format_string
|
||||
if_else = closures.if_else
|
||||
|
||||
@GObject.Property(type=Game)
|
||||
def game(self) -> Game:
|
||||
"""The game whose details to show."""
|
||||
@@ -145,14 +151,6 @@ class GameDetails(Adw.NavigationPage):
|
||||
else:
|
||||
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()
|
||||
def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None:
|
||||
if cover and (renderer := cast(Gtk.Native, self.props.root).get_renderer()):
|
||||
@@ -163,10 +161,10 @@ class GameDetails(Adw.NavigationPage):
|
||||
return None
|
||||
|
||||
@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)
|
||||
now = datetime.now(UTC)
|
||||
return label.format(
|
||||
return (
|
||||
_("Never")
|
||||
if not timestamp
|
||||
else _("Today")
|
||||
@@ -188,10 +186,6 @@ class GameDetails(Adw.NavigationPage):
|
||||
else date.strftime("%Y")
|
||||
)
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def _bool(self, _obj, o: object) -> bool:
|
||||
return bool(o)
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def _format_more_info(self, _obj, label: str) -> str:
|
||||
executable = _("program")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
python.install_sources(
|
||||
files(
|
||||
'__init__.py',
|
||||
'closures.py',
|
||||
'collection_details.py',
|
||||
'collections.py',
|
||||
'cover.py',
|
||||
|
||||
@@ -213,16 +213,16 @@ template $Window: Adw.ApplicationWindow {
|
||||
|
||||
content: Adw.ToastOverlay toast_overlay {
|
||||
child: Adw.ViewStack {
|
||||
visible-child-name: bind $_if_else(
|
||||
visible-child-name: bind $if_else(
|
||||
grid.model as <NoSelection>.n-items,
|
||||
"grid",
|
||||
$_if_else(
|
||||
$if_else(
|
||||
template.search-text,
|
||||
"empty-search",
|
||||
$_if_else(
|
||||
$if_else(
|
||||
template.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>;
|
||||
@@ -310,7 +310,10 @@ template $Window: Adw.ApplicationWindow {
|
||||
|
||||
child: Adw.StatusPage {
|
||||
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>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from cartridges import STATE_SETTINGS
|
||||
from cartridges.collections import Collection
|
||||
from cartridges.config import PREFIX, PROFILE
|
||||
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 .collections import CollectionFilter, CollectionSidebarItem
|
||||
@@ -73,6 +73,9 @@ class Window(Adw.ApplicationWindow):
|
||||
_collection_removed_signal: int | None = None
|
||||
_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."""
|
||||
@@ -232,14 +235,6 @@ class Window(Adw.ApplicationWindow):
|
||||
Gamepad.window = self # 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()
|
||||
def _show_details(self, grid: Gtk.GridView, position: int):
|
||||
self.details.game = cast(Gio.ListModel, grid.props.model).get_item(position)
|
||||
|
||||
@@ -6,6 +6,7 @@ cartridges/sources/__init__.py
|
||||
cartridges/sources/heroic.py
|
||||
cartridges/sources/imported.py
|
||||
cartridges/sources/steam.py
|
||||
cartridges/ui/closures.py
|
||||
cartridges/ui/collection-details.blp
|
||||
cartridges/ui/collection_details.py
|
||||
cartridges/ui/collections.py
|
||||
|
||||
Reference in New Issue
Block a user