window: Hook up actions to UI

Co-authored-by: Jamie Gravendeel <me@jamie.garden>
This commit is contained in:
kramo
2025-12-01 02:13:36 +01:00
parent 9917464d4e
commit 2dba556086
12 changed files with 239 additions and 32 deletions

View File

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

View File

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

View File

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

View File

@@ -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 <Picture>.width-request;
tightening-threshold: bind cover.picture as <Picture>.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 <bool>;
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;
}
}

View File

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

View File

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

View File

@@ -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%);
}
}

View File

@@ -2,6 +2,7 @@
<gresources>
<gresource prefix="@PREFIX@">
<file>cover.ui</file>
<file>game-item.ui</file>
<file>window.ui</file>
<file>style.css</file>
</gresource>

View File

@@ -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 <string>;
label: bind $_date_label(_("Last played: {}"), template.active-game as <$Game>.last_played) as <string>;
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",
]
}
}
}
}
};
};

View File

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

View File

@@ -22,8 +22,5 @@
</choices>
<default>"last_played"</default>
</key>
<key name="show-hidden" type="b">
<default>false</default>
</key>
</schema>
</schemalist>

View File

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