window: Add sorting and filtering
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
1
data/icons/filter-symbolic.svg
Normal file
1
data/icons/filter-symbolic.svg
Normal 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 |
@@ -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>
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user