window: Hook up actions to UI
Co-authored-by: Jamie Gravendeel <me@jamie.garden>
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
80
cartridges/ui/game-item.blp
Normal file
80
cartridges/ui/game-item.blp
Normal 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;
|
||||
}
|
||||
}
|
||||
30
cartridges/ui/game_item.py
Normal file
30
cartridges/ui/game_item.py
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -22,8 +22,5 @@
|
||||
</choices>
|
||||
<default>"last_played"</default>
|
||||
</key>
|
||||
<key name="show-hidden" type="b">
|
||||
<default>false</default>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user