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 types import UnionType
|
||||||
from typing import Any
|
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
|
from cartridges import DATA_DIR
|
||||||
|
|
||||||
@@ -77,9 +77,20 @@ class Game(Gio.SimpleActionGroup):
|
|||||||
|
|
||||||
setattr(self, name, value)
|
setattr(self, name, value)
|
||||||
|
|
||||||
self.add_action_entries((("play", lambda *_: self.play()),))
|
self.add_action_entries((
|
||||||
self.add_action(Gio.PropertyAction.new("hide", self, "hidden"))
|
("play", lambda *_: self.play()),
|
||||||
self.add_action(Gio.PropertyAction.new("remove", self, "removed"))
|
("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):
|
def play(self):
|
||||||
"""Run the executable command in a shell."""
|
"""Run the executable command in a shell."""
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ template $Cover: Adw.Bin {
|
|||||||
child: Adw.Clamp {
|
child: Adw.Clamp {
|
||||||
orientation: vertical;
|
orientation: vertical;
|
||||||
unit: px;
|
unit: px;
|
||||||
maximum-size: bind picture.height-request;
|
maximum-size: bind _picture.height-request;
|
||||||
tightening-threshold: bind picture.height-request;
|
tightening-threshold: bind _picture.height-request;
|
||||||
|
|
||||||
child: Adw.Clamp {
|
child: Adw.Clamp {
|
||||||
unit: px;
|
unit: px;
|
||||||
maximum-size: bind picture.width-request;
|
maximum-size: bind _picture.width-request;
|
||||||
tightening-threshold: bind picture.width-request;
|
tightening-threshold: bind _picture.width-request;
|
||||||
|
|
||||||
child: Stack {
|
child: Stack {
|
||||||
visible-child-name: bind $_get_stack_child(template.paintable) as <string>;
|
visible-child-name: bind $_get_stack_child(template.paintable) as <string>;
|
||||||
@@ -20,7 +20,7 @@ template $Cover: Adw.Bin {
|
|||||||
StackPage {
|
StackPage {
|
||||||
name: "cover";
|
name: "cover";
|
||||||
|
|
||||||
child: Picture picture {
|
child: Picture _picture {
|
||||||
paintable: bind template.paintable;
|
paintable: bind template.paintable;
|
||||||
width-request: 200;
|
width-request: 200;
|
||||||
height-request: 300;
|
height-request: 300;
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ class Cover(Adw.Bin):
|
|||||||
|
|
||||||
__gtype_name__ = __qualname__
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
|
picture = GObject.Property(lambda self: self._picture, type=Gtk.Picture)
|
||||||
paintable = GObject.Property(type=Gdk.Paintable)
|
paintable = GObject.Property(type=Gdk.Paintable)
|
||||||
app_icon_name = GObject.Property(type=str, default=f"{APP_ID}-symbolic")
|
app_icon_name = GObject.Property(type=str, default=f"{APP_ID}-symbolic")
|
||||||
|
|
||||||
|
_picture = Gtk.Template.Child()
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _get_stack_child(self, _obj, paintable: Gdk.Paintable | None) -> str:
|
def _get_stack_child(self, _obj, paintable: Gdk.Paintable | None) -> str:
|
||||||
return "cover" if paintable else "icon"
|
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(
|
python.install_sources(
|
||||||
files('__init__.py', 'cover.py', 'window.py'),
|
files('__init__.py', 'cover.py', 'game_item.py', 'window.py'),
|
||||||
subdir: 'cartridges' / 'ui',
|
subdir: 'cartridges' / 'ui',
|
||||||
)
|
)
|
||||||
|
|
||||||
blueprints = custom_target(
|
blueprints = custom_target(
|
||||||
input: files('cover.blp', 'window.blp'),
|
input: files('cover.blp', 'game-item.blp', 'window.blp'),
|
||||||
output: '.',
|
output: '.',
|
||||||
command: [
|
command: [
|
||||||
blueprint_compiler,
|
blueprint_compiler,
|
||||||
|
|||||||
@@ -11,12 +11,29 @@
|
|||||||
border-radius: 24px;
|
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 {
|
#background {
|
||||||
filter: saturate(300%) opacity(0.5);
|
filter: saturate(300%) opacity(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
|
.overlaid {
|
||||||
|
backdrop-filter: blur(9px) brightness(20%) saturate(600%);
|
||||||
|
}
|
||||||
|
|
||||||
#background {
|
#background {
|
||||||
filter: saturate(300%) opacity(0.2);
|
filter: saturate(300%) opacity(20%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="@PREFIX@">
|
<gresource prefix="@PREFIX@">
|
||||||
<file>cover.ui</file>
|
<file>cover.ui</file>
|
||||||
|
<file>game-item.ui</file>
|
||||||
<file>window.ui</file>
|
<file>window.ui</file>
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
last_played_label.justify: center;
|
last_played_label.justify: center;
|
||||||
added_label.halign: center;
|
added_label.halign: center;
|
||||||
added_label.justify: 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 {};
|
sorter: CustomSorter sorter {};
|
||||||
|
|
||||||
model: FilterListModel {
|
model: FilterListModel {
|
||||||
|
watch-items: true;
|
||||||
|
|
||||||
filter: EveryFilter {
|
filter: EveryFilter {
|
||||||
AnyFilter {
|
AnyFilter {
|
||||||
StringFilter {
|
StringFilter {
|
||||||
@@ -171,18 +176,8 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
|
|
||||||
factory: BuilderListItemFactory {
|
factory: BuilderListItemFactory {
|
||||||
template ListItem {
|
template ListItem {
|
||||||
child: Box {
|
child: $GameItem {
|
||||||
orientation: vertical;
|
game: bind template.item;
|
||||||
spacing: 12;
|
|
||||||
|
|
||||||
$Cover {
|
|
||||||
paintable: bind template.item as <$Game>.cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
label: bind template.item as <$Game>.name;
|
|
||||||
ellipsize: middle;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -232,7 +227,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
Box {
|
Box {
|
||||||
orientation: vertical;
|
orientation: vertical;
|
||||||
valign: center;
|
valign: center;
|
||||||
spacing: 3;
|
spacing: 6;
|
||||||
|
|
||||||
Label name_label {
|
Label name_label {
|
||||||
label: bind template.active-game as <$Game>.name;
|
label: bind template.active-game as <$Game>.name;
|
||||||
@@ -266,7 +261,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
spacing: 9;
|
spacing: 9;
|
||||||
|
|
||||||
Label last_played_label {
|
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;
|
halign: start;
|
||||||
wrap: true;
|
wrap: true;
|
||||||
wrap-mode: word_char;
|
wrap-mode: word_char;
|
||||||
@@ -279,6 +274,64 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
wrap-mode: word_char;
|
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 cartridges.games import Game
|
||||||
|
|
||||||
from .cover import Cover # noqa: F401
|
from .cover import Cover # noqa: F401
|
||||||
|
from .game_item import GameItem # noqa: F401
|
||||||
|
|
||||||
SORT_MODES = {
|
SORT_MODES = {
|
||||||
"last_played": ("last-played", True),
|
"last_played": ("last-played", True),
|
||||||
@@ -36,7 +37,6 @@ class Window(Adw.ApplicationWindow):
|
|||||||
grid: Gtk.GridView = Gtk.Template.Child()
|
grid: Gtk.GridView = Gtk.Template.Child()
|
||||||
sorter: Gtk.CustomSorter = Gtk.Template.Child()
|
sorter: Gtk.CustomSorter = Gtk.Template.Child()
|
||||||
|
|
||||||
active_game = GObject.Property(type=Game)
|
|
||||||
search_text = GObject.Property(type=str)
|
search_text = GObject.Property(type=str)
|
||||||
show_hidden = GObject.Property(type=bool, default=False)
|
show_hidden = GObject.Property(type=bool, default=False)
|
||||||
|
|
||||||
@@ -45,6 +45,16 @@ class Window(Adw.ApplicationWindow):
|
|||||||
"""Model of the user's games."""
|
"""Model of the user's games."""
|
||||||
return games.model
|
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):
|
def __init__(self, **kwargs: Any):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@@ -55,7 +65,6 @@ class Window(Adw.ApplicationWindow):
|
|||||||
"width": "default-width",
|
"width": "default-width",
|
||||||
"height": "default-height",
|
"height": "default-height",
|
||||||
"is-maximized": "maximized",
|
"is-maximized": "maximized",
|
||||||
"show-hidden": "show-hidden",
|
|
||||||
}.items():
|
}.items():
|
||||||
state_settings.bind(key, self, name, Gio.SettingsBindFlags.DEFAULT)
|
state_settings.bind(key, self, name, Gio.SettingsBindFlags.DEFAULT)
|
||||||
|
|
||||||
@@ -119,6 +128,10 @@ class Window(Adw.ApplicationWindow):
|
|||||||
def _bool(self, _obj, o: object) -> bool:
|
def _bool(self, _obj, o: object) -> bool:
|
||||||
return bool(o)
|
return bool(o)
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _pop(self, _obj):
|
||||||
|
self.navigation_view.pop()
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _search_started(self, entry: Gtk.SearchEntry):
|
def _search_started(self, entry: Gtk.SearchEntry):
|
||||||
entry.grab_focus()
|
entry.grab_focus()
|
||||||
|
|||||||
@@ -22,8 +22,5 @@
|
|||||||
</choices>
|
</choices>
|
||||||
<default>"last_played"</default>
|
<default>"last_played"</default>
|
||||||
</key>
|
</key>
|
||||||
<key name="show-hidden" type="b">
|
|
||||||
<default>false</default>
|
|
||||||
</key>
|
|
||||||
</schema>
|
</schema>
|
||||||
</schemalist>
|
</schemalist>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ cartridges/application.py
|
|||||||
cartridges/games.py
|
cartridges/games.py
|
||||||
cartridges/ui/cover.blp
|
cartridges/ui/cover.blp
|
||||||
cartridges/ui/cover.py
|
cartridges/ui/cover.py
|
||||||
|
cartridges/ui/game-item.blp
|
||||||
|
cartridges/ui/game_item.py
|
||||||
cartridges/ui/window.blp
|
cartridges/ui/window.blp
|
||||||
cartridges/ui/window.py
|
cartridges/ui/window.py
|
||||||
data/page.kramo.Cartridges.desktop.in
|
data/page.kramo.Cartridges.desktop.in
|
||||||
|
|||||||
Reference in New Issue
Block a user