diff --git a/cartridges/games.py b/cartridges/games.py index bdd6f27..74ec128 100644 --- a/cartridges/games.py +++ b/cartridges/games.py @@ -5,6 +5,7 @@ import itertools import json +import locale import os import subprocess from collections.abc import Generator, Iterable @@ -12,11 +13,11 @@ from json import JSONDecodeError from pathlib import Path from shlex import quote from types import UnionType -from typing import Any, NamedTuple, Self, cast +from typing import Any, NamedTuple, Self, cast, override from gi.repository import Gdk, Gio, GLib, GObject, Gtk -from cartridges import DATA_DIR +from cartridges import DATA_DIR, state_settings class _GameProp(NamedTuple): @@ -45,6 +46,13 @@ _COVERS_DIR = DATA_DIR / "covers" _SPEC_VERSION = 2.0 _MANUALLY_ADDED_ID = "imported" +_SORT_MODES = { + "last_played": ("last-played", True), + "a-z": ("name", False), + "z-a": ("name", True), + "newest": ("added", True), + "oldest": ("added", False), +} class Game(Gio.SimpleActionGroup): @@ -143,6 +151,39 @@ class Game(Gio.SimpleActionGroup): json.dump(properties, f, indent=4) +class GameSorter(Gtk.Sorter): + """A sorter for game objects. + + Automatically updates if the "sort-mode" GSetting changes. + """ + + __gtype_name__ = __qualname__ + + def __init__(self, **kwargs: Any): + super().__init__(**kwargs) + + state_settings.connect( + "changed::sort-mode", lambda *_: self.changed(Gtk.SorterChange.DIFFERENT) + ) + + @override + def do_compare(self, game1: Game, game2: Game) -> Gtk.Ordering: # pyright: ignore[reportIncompatibleMethodOverride] + prop, invert = _SORT_MODES[state_settings.get_string("sort-mode")] + a = (game2 if invert else game1).get_property(prop) + b = (game1 if invert else game2).get_property(prop) + + return Gtk.Ordering( + self._name_cmp(a, b) + if isinstance(a, str) + else ((a > b) - (a < b)) or self._name_cmp(game1.name, game2.name) + ) + + @staticmethod + def _name_cmp(a: str, b: str) -> int: + a, b = (name.lower().removeprefix("the ") for name in (a, b)) + return max(-1, min(locale.strcoll(a, b), 1)) + + def _increment_manually_added_id() -> int: numbers = { game.game_id.split("_")[1] diff --git a/cartridges/ui/window.blp b/cartridges/ui/window.blp index d812a01..55615a5 100644 --- a/cartridges/ui/window.blp +++ b/cartridges/ui/window.blp @@ -63,31 +63,31 @@ template $Window: Adw.ApplicationWindow { item { label: _("Last Played"); - action: "win.sort"; + action: "win.sort-mode"; target: "last_played"; } item { label: _("A-Z"); - action: "win.sort"; + action: "win.sort-mode"; target: "a-z"; } item { label: _("Z-A"); - action: "win.sort"; + action: "win.sort-mode"; target: "z-a"; } item { label: _("Newest"); - action: "win.sort"; + action: "win.sort-mode"; target: "newest"; } item { label: _("Oldest"); - action: "win.sort"; + action: "win.sort-mode"; target: "oldest"; } } @@ -137,7 +137,7 @@ template $Window: Adw.ApplicationWindow { model: NoSelection { model: SortListModel { - sorter: CustomSorter sorter {}; + sorter: $GameSorter sorter {}; model: FilterListModel { watch-items: true; diff --git a/cartridges/ui/window.py b/cartridges/ui/window.py index 3c2155e..b6b3f24 100644 --- a/cartridges/ui/window.py +++ b/cartridges/ui/window.py @@ -3,15 +3,13 @@ # SPDX-FileCopyrightText: Copyright 2022-2025 kramo # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel -import locale -from collections.abc import Generator from typing import Any, TypeVar, cast from gi.repository import Adw, Gio, GLib, GObject, Gtk from cartridges import games, state_settings from cartridges.config import PREFIX, PROFILE -from cartridges.games import Game +from cartridges.games import Game, GameSorter from .game_details import GameDetails from .game_item import GameItem # noqa: F401 @@ -36,7 +34,7 @@ class Window(Adw.ApplicationWindow): navigation_view: Adw.NavigationView = Gtk.Template.Child() search_entry: Gtk.SearchEntry = Gtk.Template.Child() grid: Gtk.GridView = Gtk.Template.Child() - sorter: Gtk.CustomSorter = Gtk.Template.Child() + sorter: GameSorter = Gtk.Template.Child() details: GameDetails = Gtk.Template.Child() search_text = GObject.Property(type=str) @@ -60,17 +58,11 @@ class Window(Adw.ApplicationWindow): # 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(state_settings.create_action("sort-mode")) self.add_action(Gio.PropertyAction.new("show-hidden", self, "show-hidden")) self.add_action_entries(( ("search", lambda *_: self.search_entry.grab_focus()), - ( - "sort", - self._sort, - "s", - state_settings.get_value("sort-mode").print_(False), - ), ("edit", lambda _action, param, *_: self._edit(param.get_uint32()), "u"), ("add", lambda *_: self._add()), )) @@ -102,35 +94,6 @@ class Window(Adw.ApplicationWindow): entry.props.text = "" self.grid.grab_focus() - def _sort(self, action: Gio.SimpleAction, parameter: GLib.Variant, *_args): - action.change_state(parameter) - sort_mode = parameter.get_string() - - prop, invert = SORT_MODES[sort_mode] - prev_prop, prev_invert = SORT_MODES[state_settings.get_string("sort-mode")] - opposite = (prev_prop == prop) and (prev_invert != invert) - - state_settings.set_string("sort-mode", sort_mode) - self.sorter.changed( - Gtk.SorterChange.INVERTED if opposite else Gtk.SorterChange.DIFFERENT - ) - - def _sort_func(self, game1: Game, game2: Game, _) -> int: - prop, invert = SORT_MODES[state_settings.get_string("sort-mode")] - a = (game2 if invert else game1).get_property(prop) - b = (game1 if invert else game2).get_property(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 _sort_changed(self, *_args): self.sorter.changed(Gtk.SorterChange.DIFFERENT)