games: Add sorter
This commit is contained in:
committed by
Laura Kramolis
parent
9d4932f22e
commit
c1e3c987c1
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
|
import locale
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections.abc import Generator, Iterable
|
from collections.abc import Generator, Iterable
|
||||||
@@ -12,11 +13,11 @@ from json import JSONDecodeError
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shlex import quote
|
from shlex import quote
|
||||||
from types import UnionType
|
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 gi.repository import Gdk, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import DATA_DIR
|
from cartridges import DATA_DIR, state_settings
|
||||||
|
|
||||||
|
|
||||||
class _GameProp(NamedTuple):
|
class _GameProp(NamedTuple):
|
||||||
@@ -45,6 +46,13 @@ _COVERS_DIR = DATA_DIR / "covers"
|
|||||||
|
|
||||||
_SPEC_VERSION = 2.0
|
_SPEC_VERSION = 2.0
|
||||||
_MANUALLY_ADDED_ID = "imported"
|
_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):
|
class Game(Gio.SimpleActionGroup):
|
||||||
@@ -143,6 +151,39 @@ class Game(Gio.SimpleActionGroup):
|
|||||||
json.dump(properties, f, indent=4)
|
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:
|
def _increment_manually_added_id() -> int:
|
||||||
numbers = {
|
numbers = {
|
||||||
game.game_id.split("_")[1]
|
game.game_id.split("_")[1]
|
||||||
|
|||||||
@@ -63,31 +63,31 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
|
|
||||||
item {
|
item {
|
||||||
label: _("Last Played");
|
label: _("Last Played");
|
||||||
action: "win.sort";
|
action: "win.sort-mode";
|
||||||
target: "last_played";
|
target: "last_played";
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
label: _("A-Z");
|
label: _("A-Z");
|
||||||
action: "win.sort";
|
action: "win.sort-mode";
|
||||||
target: "a-z";
|
target: "a-z";
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
label: _("Z-A");
|
label: _("Z-A");
|
||||||
action: "win.sort";
|
action: "win.sort-mode";
|
||||||
target: "z-a";
|
target: "z-a";
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
label: _("Newest");
|
label: _("Newest");
|
||||||
action: "win.sort";
|
action: "win.sort-mode";
|
||||||
target: "newest";
|
target: "newest";
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
label: _("Oldest");
|
label: _("Oldest");
|
||||||
action: "win.sort";
|
action: "win.sort-mode";
|
||||||
target: "oldest";
|
target: "oldest";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
|
|
||||||
model: NoSelection {
|
model: NoSelection {
|
||||||
model: SortListModel {
|
model: SortListModel {
|
||||||
sorter: CustomSorter sorter {};
|
sorter: $GameSorter sorter {};
|
||||||
|
|
||||||
model: FilterListModel {
|
model: FilterListModel {
|
||||||
watch-items: true;
|
watch-items: true;
|
||||||
|
|||||||
@@ -3,15 +3,13 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
|
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
||||||
|
|
||||||
import locale
|
|
||||||
from collections.abc import Generator
|
|
||||||
from typing import Any, TypeVar, cast
|
from typing import Any, TypeVar, cast
|
||||||
|
|
||||||
from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import games, state_settings
|
from cartridges import games, state_settings
|
||||||
from cartridges.config import PREFIX, PROFILE
|
from cartridges.config import PREFIX, PROFILE
|
||||||
from cartridges.games import Game
|
from cartridges.games import Game, GameSorter
|
||||||
|
|
||||||
from .game_details import GameDetails
|
from .game_details import GameDetails
|
||||||
from .game_item import GameItem # noqa: F401
|
from .game_item import GameItem # noqa: F401
|
||||||
@@ -36,7 +34,7 @@ class Window(Adw.ApplicationWindow):
|
|||||||
navigation_view: Adw.NavigationView = Gtk.Template.Child()
|
navigation_view: Adw.NavigationView = Gtk.Template.Child()
|
||||||
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
|
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
|
||||||
grid: Gtk.GridView = 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()
|
details: GameDetails = Gtk.Template.Child()
|
||||||
|
|
||||||
search_text = GObject.Property(type=str)
|
search_text = GObject.Property(type=str)
|
||||||
@@ -60,17 +58,11 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
|
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
|
||||||
self.search_entry.set_key_capture_widget(self)
|
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(Gio.PropertyAction.new("show-hidden", self, "show-hidden"))
|
||||||
self.add_action_entries((
|
self.add_action_entries((
|
||||||
("search", lambda *_: self.search_entry.grab_focus()),
|
("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"),
|
("edit", lambda _action, param, *_: self._edit(param.get_uint32()), "u"),
|
||||||
("add", lambda *_: self._add()),
|
("add", lambda *_: self._add()),
|
||||||
))
|
))
|
||||||
@@ -102,35 +94,6 @@ class Window(Adw.ApplicationWindow):
|
|||||||
entry.props.text = ""
|
entry.props.text = ""
|
||||||
self.grid.grab_focus()
|
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()
|
@Gtk.Template.Callback()
|
||||||
def _sort_changed(self, *_args):
|
def _sort_changed(self, *_args):
|
||||||
self.sorter.changed(Gtk.SorterChange.DIFFERENT)
|
self.sorter.changed(Gtk.SorterChange.DIFFERENT)
|
||||||
|
|||||||
Reference in New Issue
Block a user