window: Add sorting and filtering

This commit is contained in:
kramo
2025-11-29 18:50:18 +01:00
parent 35b50d3d8b
commit 49494063d7
6 changed files with 171 additions and 8 deletions

View File

@@ -11,7 +11,7 @@ from gi.repository import (
Gio,
GLib,
GObject,
Json, # pyright: ignore[reportAttributeAccessIssue, reportUnknownVariableType]
Json, # pyright: ignore[reportAttributeAccessIssue]
)
DATA_DIR = Path(GLib.get_user_data_dir(), "cartridges")

View File

@@ -7,6 +7,11 @@ template $Window: Adw.ApplicationWindow {
default-height: 700;
ShortcutController {
Shortcut {
trigger: "<Control>f";
action: "action(win.search)";
}
Shortcut {
trigger: "<Control>w";
action: "action(window.close)";
@@ -16,24 +21,114 @@ template $Window: Adw.ApplicationWindow {
content: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.Clamp {
maximum-size: 500;
tightening-threshold: 500;
child: SearchEntry search_entry {
hexpand: true;
placeholder-text: _("Search games");
search-started => $_search_started();
search-changed => $_search_changed();
stop-search => $_stop_search();
};
};
[end]
MenuButton {
tooltip-text: _("Main Menu");
icon-name: "open-menu-symbolic";
tooltip-text: _("Main Menu");
primary: true;
menu-model: menu {
item (_("About Cartridges"), "app.about")
};
}
[end]
MenuButton {
icon-name: "filter-symbolic";
tooltip-text: _("Sort & Filter");
menu-model: menu {
section {
label: _("Sort");
item {
label: _("A-Z");
action: "win.sort";
target: "a-z";
}
item {
label: _("Z-A");
action: "win.sort";
target: "z-a";
}
item {
label: _("Newest");
action: "win.sort";
target: "newest";
}
item {
label: _("Oldest");
action: "win.sort";
target: "oldest";
}
item {
label: _("Last Played");
action: "win.sort";
target: "last_played";
}
}
};
}
}
content: ScrolledWindow {
child: GridView {
child: GridView grid {
name: "grid";
model: NoSelection {
model: bind template.games;
model: SortListModel {
sorter: CustomSorter sorter {};
model: FilterListModel {
filter: EveryFilter {
AnyFilter {
StringFilter {
expression: expr item as <$Game>.name;
search: bind template.search-text;
}
StringFilter {
expression: expr item as <$Game>.developer;
search: bind template.search-text;
}
}
BoolFilter {
expression: expr item as <$Game>.hidden;
invert: true;
}
BoolFilter {
expression: expr item as <$Game>.removed;
invert: true;
}
BoolFilter {
expression: expr item as <$Game>.blacklisted;
invert: true;
}
};
model: bind template.games;
};
};
};
factory: BuilderListItemFactory {

View File

@@ -1,12 +1,23 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed
import locale
from collections.abc import Generator
from typing import Any
from gi.repository import Adw, Gio, GObject, Gtk
from gi.repository import Adw, Gio, GLib, GObject, Gtk
from cartridges import games
from cartridges.config import PREFIX, PROFILE
from cartridges.games import Game
SORT_MODES = {
"a-z": ("name", False),
"z-a": ("name", True),
"newest": ("added", False),
"oldest": ("added", True),
"last_played": ("last-played", True),
}
@Gtk.Template.from_resource(f"{PREFIX}/window.ui")
@@ -15,6 +26,15 @@ class Window(Adw.ApplicationWindow):
__gtype_name__ = __qualname__
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
grid: Gtk.GridView = Gtk.Template.Child()
sorter: Gtk.CustomSorter = Gtk.Template.Child()
search_text = GObject.Property(type=str)
_sort_prop = "name"
_invert_sort = False
@GObject.Property(type=Gio.ListStore)
def games(self) -> Gio.ListStore:
"""Model of the user's games."""
@@ -25,3 +45,50 @@ class Window(Adw.ApplicationWindow):
if PROFILE == "development":
self.add_css_class("devel")
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
self.search_entry.set_key_capture_widget(self)
self.sorter.set_sort_func(self._sort_func)
self.add_action_entries((
("search", lambda *_: self.search_entry.grab_focus()),
("sort", self._sort, "s", "'a-z'"),
))
def _sort(self, action: Gio.SimpleAction, parameter: GLib.Variant, *_args) -> None:
action.change_state(parameter)
prop, invert = SORT_MODES[parameter.get_string()]
opposite = (self._sort_prop == prop) and (self._invert_sort != invert)
self._sort_prop, self._invert_sort = prop, invert
self.sorter.changed(
Gtk.SorterChange.INVERTED if opposite else Gtk.SorterChange.DIFFERENT
)
def _sort_func(self, game1: Game, game2: Game, _) -> int:
a = (game2 if self._invert_sort else game1).get_property(self._sort_prop)
b = (game1 if self._invert_sort else game2).get_property(self._sort_prop)
return (
locale.strcoll(*self._sortable(a, b))
if isinstance(a, str)
else (a > b) - (a < b)
or locale.strcoll(*self._sortable(game1.name, game2.name))
)
@staticmethod
def _sortable(*strings: str) -> Generator[str]:
for string in strings:
yield string.lower().removeprefix("the ")
@Gtk.Template.Callback()
def _search_started(self, entry: Gtk.SearchEntry) -> None:
entry.grab_focus()
@Gtk.Template.Callback()
def _search_changed(self, entry: Gtk.SearchEntry) -> None:
self.search_text = entry.props.text
entry.grab_focus()
@Gtk.Template.Callback()
def _stop_search(self, entry: Gtk.SearchEntry) -> None:
entry.props.text = ""
self.grid.grab_focus()

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="#222"><path d="M1.5 2h13a1.5 1.5 0 0 1 0 3h-13a1.5 1.5 0 0 1 0-3M4.5 7h7a1.5 1.5 0 0 1 0 3h-7a1.5 1.5 0 0 1 0-3M7.5 12h1a1.5 1.5 0 0 1 0 3h-1a1.5 1.5 0 0 1 0-3m0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<gresources>
<gresource prefix="@PREFIX@/icons/scalable/actions">
<file>filter-symbolic.svg</file>
</gresource>
</gresources>

View File

@@ -11,6 +11,7 @@ reportUnknownArgumentType = "none"
reportUnknownLambdaType = "none"
reportUnknownMemberType = "none"
reportUnknownParameterType = "none"
reportUnknownVariableType = "none"
reportUntypedFunctionDecorator = "none"
reportUnusedImport = "none"
reportUnusedVariable = "none"
@@ -68,6 +69,4 @@ suppress-dummy-args = true
suppress-none-returning = true
[tool.ruff.lint.pydocstyle]
property-decorators = [
"gi.repository.GObject.Property",
]
property-decorators = ["gi.repository.GObject.Property"]