From 2dba556086fb161933bf1413608299bab6d0ac4b Mon Sep 17 00:00:00 2001 From: kramo Date: Mon, 1 Dec 2025 02:13:36 +0100 Subject: [PATCH] window: Hook up actions to UI Co-authored-by: Jamie Gravendeel --- cartridges/games.py | 19 ++++-- cartridges/ui/cover.blp | 10 +-- cartridges/ui/cover.py | 3 + cartridges/ui/game-item.blp | 80 ++++++++++++++++++++++ cartridges/ui/game_item.py | 30 +++++++++ cartridges/ui/meson.build | 4 +- cartridges/ui/style.css | 21 +++++- cartridges/ui/ui.gresource.xml.in | 1 + cartridges/ui/window.blp | 81 +++++++++++++++++++---- cartridges/ui/window.py | 17 ++++- data/page.kramo.Cartridges.gschema.xml.in | 3 - po/POTFILES.in | 2 + 12 files changed, 239 insertions(+), 32 deletions(-) create mode 100644 cartridges/ui/game-item.blp create mode 100644 cartridges/ui/game_item.py diff --git a/cartridges/games.py b/cartridges/games.py index cc03052..548b3fa 100644 --- a/cartridges/games.py +++ b/cartridges/games.py @@ -13,7 +13,7 @@ from shlex import quote from types import UnionType from typing import Any -from gi.repository import Gdk, Gio, GLib, GObject +from gi.repository import Gdk, Gio, GLib, GObject, Gtk from cartridges import DATA_DIR @@ -77,9 +77,20 @@ class Game(Gio.SimpleActionGroup): setattr(self, name, value) - self.add_action_entries((("play", lambda *_: self.play()),)) - self.add_action(Gio.PropertyAction.new("hide", self, "hidden")) - self.add_action(Gio.PropertyAction.new("remove", self, "removed")) + self.add_action_entries(( + ("play", lambda *_: self.play()), + ("remove", lambda *_: setattr(self, "removed", True)), + )) + + self.add_action(unhide_action := Gio.SimpleAction.new("unhide")) + unhide_action.connect("activate", lambda *_: setattr(self, "hidden", False)) + hidden = Gtk.PropertyExpression.new(Game, None, "hidden") + hidden.bind(unhide_action, "enabled", self) + + self.add_action(hide_action := Gio.SimpleAction.new("hide")) + hide_action.connect("activate", lambda *_: setattr(self, "hidden", True)) + not_hidden = Gtk.ClosureExpression.new(bool, lambda _, h: not h, (hidden,)) + not_hidden.bind(hide_action, "enabled", self) def play(self): """Run the executable command in a shell.""" diff --git a/cartridges/ui/cover.blp b/cartridges/ui/cover.blp index 0c3b456..9312217 100644 --- a/cartridges/ui/cover.blp +++ b/cartridges/ui/cover.blp @@ -5,13 +5,13 @@ template $Cover: Adw.Bin { child: Adw.Clamp { orientation: vertical; unit: px; - maximum-size: bind picture.height-request; - tightening-threshold: bind picture.height-request; + maximum-size: bind _picture.height-request; + tightening-threshold: bind _picture.height-request; child: Adw.Clamp { unit: px; - maximum-size: bind picture.width-request; - tightening-threshold: bind picture.width-request; + maximum-size: bind _picture.width-request; + tightening-threshold: bind _picture.width-request; child: Stack { visible-child-name: bind $_get_stack_child(template.paintable) as ; @@ -20,7 +20,7 @@ template $Cover: Adw.Bin { StackPage { name: "cover"; - child: Picture picture { + child: Picture _picture { paintable: bind template.paintable; width-request: 200; height-request: 300; diff --git a/cartridges/ui/cover.py b/cartridges/ui/cover.py index 918bef4..f70e376 100644 --- a/cartridges/ui/cover.py +++ b/cartridges/ui/cover.py @@ -12,9 +12,12 @@ class Cover(Adw.Bin): __gtype_name__ = __qualname__ + picture = GObject.Property(lambda self: self._picture, type=Gtk.Picture) paintable = GObject.Property(type=Gdk.Paintable) app_icon_name = GObject.Property(type=str, default=f"{APP_ID}-symbolic") + _picture = Gtk.Template.Child() + @Gtk.Template.Callback() def _get_stack_child(self, _obj, paintable: Gdk.Paintable | None) -> str: return "cover" if paintable else "icon" diff --git a/cartridges/ui/game-item.blp b/cartridges/ui/game-item.blp new file mode 100644 index 0000000..f0d6ae4 --- /dev/null +++ b/cartridges/ui/game-item.blp @@ -0,0 +1,80 @@ +using Gtk 4.0; +using Adw 1; + +template $GameItem: Box { + name: "game-item"; + orientation: vertical; + spacing: 12; + + EventControllerMotion motion {} + + Adw.Clamp { + unit: px; + maximum-size: bind cover.picture as .width-request; + tightening-threshold: bind cover.picture as .width-request; + + child: Overlay { + child: $Cover cover { + paintable: bind template.game as <$Game>.cover; + }; + + [overlay] + Revealer { + reveal-child: bind $_any(motion.contains-pointer, options.active) as ; + transition-type: slide_down; // https://gitlab.gnome.org/GNOME/gtk/-/issues/7903 + halign: end; + valign: start; + + child: MenuButton options { + icon-name: "view-more-symbolic"; + tooltip-text: _("Options"); + margin-top: 6; + margin-end: 6; + + menu-model: menu { + item { + action: "game.hide"; + label: _("Hide"); + hidden-when: "action-disabled"; + } + + item { + action: "game.unhide"; + label: _("Unhide"); + hidden-when: "action-disabled"; + } + + item (_("Remove"), "game.remove") + }; + + styles [ + "circular", + ] + }; + } + + [overlay] + Revealer { + reveal-child: bind motion.contains-pointer; + transition-type: slide_up; // https://gitlab.gnome.org/GNOME/gtk/-/issues/7903 + halign: center; + valign: end; + + child: Button { + action-name: "game.play"; + label: _("Play"); + margin-bottom: 12; + + styles [ + "pill", + ] + }; + } + }; + } + + Label { + label: bind template.game as <$Game>.name; + ellipsize: middle; + } +} diff --git a/cartridges/ui/game_item.py b/cartridges/ui/game_item.py new file mode 100644 index 0000000..6970bbc --- /dev/null +++ b/cartridges/ui/game_item.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: Copyright 2025 kramo + +from gi.repository import GObject, Gtk + +from cartridges.config import PREFIX +from cartridges.games import Game + +from .cover import Cover # noqa: F401 + + +@Gtk.Template.from_resource(f"{PREFIX}/game-item.ui") +class GameItem(Gtk.Box): + """A game in the grid.""" + + __gtype_name__ = __qualname__ + + @GObject.Property(type=Game) + def game(self) -> Game | None: + """The game that `self` represents.""" + return self._game + + @game.setter + def game(self, game: Game | None): + self._game = game + self.insert_action_group("game", game) + + @Gtk.Template.Callback() + def _any(self, _obj, *values: bool) -> bool: + return any(values) diff --git a/cartridges/ui/meson.build b/cartridges/ui/meson.build index 72c469a..03a29ea 100644 --- a/cartridges/ui/meson.build +++ b/cartridges/ui/meson.build @@ -1,10 +1,10 @@ python.install_sources( - files('__init__.py', 'cover.py', 'window.py'), + files('__init__.py', 'cover.py', 'game_item.py', 'window.py'), subdir: 'cartridges' / 'ui', ) blueprints = custom_target( - input: files('cover.blp', 'window.blp'), + input: files('cover.blp', 'game-item.blp', 'window.blp'), output: '.', command: [ blueprint_compiler, diff --git a/cartridges/ui/style.css b/cartridges/ui/style.css index 2b24060..af70d61 100644 --- a/cartridges/ui/style.css +++ b/cartridges/ui/style.css @@ -11,12 +11,29 @@ border-radius: 24px; } +#game-item button { + color: white; + backdrop-filter: blur(9px) brightness(30%) saturate(600%); + box-shadow: + 0 0 0 1px rgb(from currentcolor r g b / 10%) inset, + 0 1px rgb(from currentcolor r g b / 30%) inset; +} + +#details-play-button { + --accent-fg-color: var(--view-bg-color); + --accent-bg-color: var(--view-fg-color); +} + #background { - filter: saturate(300%) opacity(0.5); + filter: saturate(300%) opacity(50%); } @media (prefers-contrast: more) { + .overlaid { + backdrop-filter: blur(9px) brightness(20%) saturate(600%); + } + #background { - filter: saturate(300%) opacity(0.2); + filter: saturate(300%) opacity(20%); } } diff --git a/cartridges/ui/ui.gresource.xml.in b/cartridges/ui/ui.gresource.xml.in index f15d90d..2c88eba 100644 --- a/cartridges/ui/ui.gresource.xml.in +++ b/cartridges/ui/ui.gresource.xml.in @@ -2,6 +2,7 @@ cover.ui + game-item.ui window.ui style.css diff --git a/cartridges/ui/window.blp b/cartridges/ui/window.blp index 7fb7369..d90fcc1 100644 --- a/cartridges/ui/window.blp +++ b/cartridges/ui/window.blp @@ -37,6 +37,9 @@ template $Window: Adw.ApplicationWindow { last_played_label.justify: center; added_label.halign: center; added_label.justify: center; + actions.orientation: vertical; + actions.halign: center; + actions.spacing: 24; } } @@ -135,6 +138,8 @@ template $Window: Adw.ApplicationWindow { sorter: CustomSorter sorter {}; model: FilterListModel { + watch-items: true; + filter: EveryFilter { AnyFilter { StringFilter { @@ -171,18 +176,8 @@ template $Window: Adw.ApplicationWindow { factory: BuilderListItemFactory { template ListItem { - child: Box { - orientation: vertical; - spacing: 12; - - $Cover { - paintable: bind template.item as <$Game>.cover; - } - - Label { - label: bind template.item as <$Game>.name; - ellipsize: middle; - } + child: $GameItem { + game: bind template.item; }; } }; @@ -232,7 +227,7 @@ template $Window: Adw.ApplicationWindow { Box { orientation: vertical; valign: center; - spacing: 3; + spacing: 6; Label name_label { label: bind template.active-game as <$Game>.name; @@ -266,7 +261,7 @@ template $Window: Adw.ApplicationWindow { spacing: 9; Label last_played_label { - label: bind $_date_label(_("Last Played: {}"), template.active-game as <$Game>.last_played) as ; + label: bind $_date_label(_("Last played: {}"), template.active-game as <$Game>.last_played) as ; halign: start; wrap: true; wrap-mode: word_char; @@ -279,6 +274,64 @@ template $Window: Adw.ApplicationWindow { wrap-mode: word_char; } } + + Box actions { + spacing: 12; + margin-top: 15; + + Button { + name: "details-play-button"; + action-name: "game.play"; + label: _("Play"); + valign: center; + + styles [ + "pill", + "suggested-action", + ] + } + + Box { + spacing: 6; + halign: center; + + Button hide_button { + visible: bind hide_button.sensitive; + action-name: "game.hide"; + icon-name: "view-conceal-symbolic"; + tooltip-text: _("Hide"); + valign: center; + + styles [ + "circular", + ] + } + + Button unhide_button { + visible: bind unhide_button.sensitive; + action-name: "game.unhide"; + icon-name: "view-reveal-symbolic"; + tooltip-text: _("Unhide"); + valign: center; + + styles [ + "circular", + ] + } + + Button { + action-name: "game.remove"; + icon-name: "user-trash-symbolic"; + tooltip-text: _("Remove"); + valign: center; + clicked => $_pop(); + + styles [ + "circular", + ] + } + } + } } }; }; diff --git a/cartridges/ui/window.py b/cartridges/ui/window.py index de06677..a0c7773 100644 --- a/cartridges/ui/window.py +++ b/cartridges/ui/window.py @@ -15,6 +15,7 @@ from cartridges.config import PREFIX, PROFILE from cartridges.games import Game from .cover import Cover # noqa: F401 +from .game_item import GameItem # noqa: F401 SORT_MODES = { "last_played": ("last-played", True), @@ -36,7 +37,6 @@ class Window(Adw.ApplicationWindow): grid: Gtk.GridView = Gtk.Template.Child() sorter: Gtk.CustomSorter = Gtk.Template.Child() - active_game = GObject.Property(type=Game) search_text = GObject.Property(type=str) show_hidden = GObject.Property(type=bool, default=False) @@ -45,6 +45,16 @@ class Window(Adw.ApplicationWindow): """Model of the user's games.""" return games.model + @GObject.Property(type=Game) + def active_game(self) -> Game | None: + """The game whose details to show.""" + return self._active_game + + @active_game.setter + def active_game(self, active_game: Game | None): + self._active_game = active_game + self.insert_action_group("game", active_game) + def __init__(self, **kwargs: Any): super().__init__(**kwargs) @@ -55,7 +65,6 @@ class Window(Adw.ApplicationWindow): "width": "default-width", "height": "default-height", "is-maximized": "maximized", - "show-hidden": "show-hidden", }.items(): state_settings.bind(key, self, name, Gio.SettingsBindFlags.DEFAULT) @@ -119,6 +128,10 @@ class Window(Adw.ApplicationWindow): def _bool(self, _obj, o: object) -> bool: return bool(o) + @Gtk.Template.Callback() + def _pop(self, _obj): + self.navigation_view.pop() + @Gtk.Template.Callback() def _search_started(self, entry: Gtk.SearchEntry): entry.grab_focus() diff --git a/data/page.kramo.Cartridges.gschema.xml.in b/data/page.kramo.Cartridges.gschema.xml.in index b3d9fd4..84517fb 100644 --- a/data/page.kramo.Cartridges.gschema.xml.in +++ b/data/page.kramo.Cartridges.gschema.xml.in @@ -22,8 +22,5 @@ "last_played" - - false - diff --git a/po/POTFILES.in b/po/POTFILES.in index f291c7e..1912b2c 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -2,6 +2,8 @@ cartridges/application.py cartridges/games.py cartridges/ui/cover.blp cartridges/ui/cover.py +cartridges/ui/game-item.blp +cartridges/ui/game_item.py cartridges/ui/window.blp cartridges/ui/window.py data/page.kramo.Cartridges.desktop.in