window: Add sorting and filtering
This commit is contained in:
@@ -11,7 +11,7 @@ from gi.repository import (
|
|||||||
Gio,
|
Gio,
|
||||||
GLib,
|
GLib,
|
||||||
GObject,
|
GObject,
|
||||||
Json, # pyright: ignore[reportAttributeAccessIssue, reportUnknownVariableType]
|
Json, # pyright: ignore[reportAttributeAccessIssue]
|
||||||
)
|
)
|
||||||
|
|
||||||
DATA_DIR = Path(GLib.get_user_data_dir(), "cartridges")
|
DATA_DIR = Path(GLib.get_user_data_dir(), "cartridges")
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
default-height: 700;
|
default-height: 700;
|
||||||
|
|
||||||
ShortcutController {
|
ShortcutController {
|
||||||
|
Shortcut {
|
||||||
|
trigger: "<Control>f";
|
||||||
|
action: "action(win.search)";
|
||||||
|
}
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
trigger: "<Control>w";
|
trigger: "<Control>w";
|
||||||
action: "action(window.close)";
|
action: "action(window.close)";
|
||||||
@@ -16,24 +21,114 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
content: Adw.ToolbarView {
|
content: Adw.ToolbarView {
|
||||||
[top]
|
[top]
|
||||||
Adw.HeaderBar {
|
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]
|
[end]
|
||||||
MenuButton {
|
MenuButton {
|
||||||
tooltip-text: _("Main Menu");
|
|
||||||
icon-name: "open-menu-symbolic";
|
icon-name: "open-menu-symbolic";
|
||||||
|
tooltip-text: _("Main Menu");
|
||||||
primary: true;
|
primary: true;
|
||||||
|
|
||||||
menu-model: menu {
|
menu-model: menu {
|
||||||
item (_("About Cartridges"), "app.about")
|
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 {
|
content: ScrolledWindow {
|
||||||
child: GridView {
|
child: GridView grid {
|
||||||
name: "grid";
|
name: "grid";
|
||||||
|
|
||||||
model: NoSelection {
|
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 {
|
factory: BuilderListItemFactory {
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed
|
# SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed
|
||||||
|
|
||||||
|
import locale
|
||||||
|
from collections.abc import Generator
|
||||||
from typing import Any
|
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 import games
|
||||||
from cartridges.config import PREFIX, PROFILE
|
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")
|
@Gtk.Template.from_resource(f"{PREFIX}/window.ui")
|
||||||
@@ -15,6 +26,15 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
__gtype_name__ = __qualname__
|
__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)
|
@GObject.Property(type=Gio.ListStore)
|
||||||
def games(self) -> Gio.ListStore:
|
def games(self) -> Gio.ListStore:
|
||||||
"""Model of the user's games."""
|
"""Model of the user's games."""
|
||||||
@@ -25,3 +45,50 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
if PROFILE == "development":
|
if PROFILE == "development":
|
||||||
self.add_css_class("devel")
|
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" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="@PREFIX@/icons/scalable/actions">
|
<gresource prefix="@PREFIX@/icons/scalable/actions">
|
||||||
|
<file>filter-symbolic.svg</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ reportUnknownArgumentType = "none"
|
|||||||
reportUnknownLambdaType = "none"
|
reportUnknownLambdaType = "none"
|
||||||
reportUnknownMemberType = "none"
|
reportUnknownMemberType = "none"
|
||||||
reportUnknownParameterType = "none"
|
reportUnknownParameterType = "none"
|
||||||
|
reportUnknownVariableType = "none"
|
||||||
reportUntypedFunctionDecorator = "none"
|
reportUntypedFunctionDecorator = "none"
|
||||||
reportUnusedImport = "none"
|
reportUnusedImport = "none"
|
||||||
reportUnusedVariable = "none"
|
reportUnusedVariable = "none"
|
||||||
@@ -68,6 +69,4 @@ suppress-dummy-args = true
|
|||||||
suppress-none-returning = true
|
suppress-none-returning = true
|
||||||
|
|
||||||
[tool.ruff.lint.pydocstyle]
|
[tool.ruff.lint.pydocstyle]
|
||||||
property-decorators = [
|
property-decorators = ["gi.repository.GObject.Property"]
|
||||||
"gi.repository.GObject.Property",
|
|
||||||
]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user