tracklist: Add a SortOrderModel
This is a Gio.ListStore with some extra functions for enabling, disabling, rearranging, and reversing sort fields. It also has a sort-string property for getting the current sort order to save to the database. Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
b326320156
commit
83355f7e96
|
@ -3,6 +3,8 @@
|
|||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
|
||||
SORT_ORDER_FLAGS = GObject.ParamFlags.READWRITE | \
|
||||
GObject.ParamFlags.EXPLICIT_NOTIFY
|
||||
SORT_FIELDS = {"Album": ["album"], "Artist": ["artist"],
|
||||
"Album Artist": ["albumartist"],
|
||||
"Filepath": ["filepath"], "Length": ["length"],
|
||||
|
@ -10,6 +12,7 @@ SORT_FIELDS = {"Album": ["album"], "Artist": ["artist"],
|
|||
"Last Started Date": ["laststarted"],
|
||||
"Play Count": ["playcount"], "Release Date": ["release"],
|
||||
"Title": ["title"], "Track Number": ["mediumno", "number"]}
|
||||
REVERSE_LOOKUP = {v: k for k, l in SORT_FIELDS.items() for v in l}
|
||||
|
||||
|
||||
class SortField(GObject.GObject):
|
||||
|
@ -53,3 +56,99 @@ class SortField(GObject.GObject):
|
|||
def reverse(self) -> None:
|
||||
"""Reverse the direction of this Sort Field."""
|
||||
self.model.reverse(self)
|
||||
|
||||
|
||||
class SortOrderModel(Gio.ListStore):
|
||||
"""A ListModel for managing Sort Order."""
|
||||
|
||||
n_enabled = GObject.Property(type=int, default=0)
|
||||
sort_order = GObject.Property(type=str)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Sort Order Model."""
|
||||
super().__init__(item_type=SortField)
|
||||
for name in SORT_FIELDS.keys():
|
||||
self.append(SortField(model=self, name=name))
|
||||
|
||||
def __move_field(self, field: SortField, position: int,
|
||||
*, update: bool) -> None:
|
||||
self.remove(self.index(field))
|
||||
self.insert(position, field)
|
||||
if update:
|
||||
self.__update_sort_order()
|
||||
|
||||
def __update_sort_order(self) -> None:
|
||||
enabled = list(filter(lambda f: f.enabled, self))
|
||||
self.n_enabled = len(enabled)
|
||||
self.sort_order = ", ".join([str(f) for f in enabled])
|
||||
|
||||
def disable(self, field: SortField) -> bool:
|
||||
"""Disable a SortField."""
|
||||
if field.enabled:
|
||||
field.enabled = False
|
||||
field.reversed = False
|
||||
self.__move_field(field, self.n_enabled - 1, update=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def enable(self, field: SortField) -> bool:
|
||||
"""Enable a SortField."""
|
||||
if not field.enabled:
|
||||
field.enabled = True
|
||||
self.__move_field(field, self.n_enabled, update=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def index(self, field: SortField) -> int | None:
|
||||
"""Find the index of a specific field."""
|
||||
(found, position) = self.find(field)
|
||||
return position if found else None
|
||||
|
||||
def lookup(self, name: str) -> SortField | None:
|
||||
"""Find a SortField by name."""
|
||||
for field in self:
|
||||
if field.name == name:
|
||||
return field
|
||||
|
||||
def move_down(self, field: SortField) -> bool:
|
||||
"""Move a SortField one position closer to the end of the list."""
|
||||
if (index := self.index(field)) < self.n_enabled - 1:
|
||||
self.__move_field(field, index + 1, update=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def move_up(self, field: SortField) -> bool:
|
||||
"""Move a SortField one position closer to the start of the list."""
|
||||
if 0 < (index := self.index(field)) < self.n_enabled:
|
||||
self.__move_field(field, index - 1, update=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def reverse(self, field: SortField) -> None:
|
||||
"""Reverse the ordering of a Sort Field."""
|
||||
field.reversed = not field.reversed
|
||||
self.__update_sort_order()
|
||||
|
||||
def set_sort_order(self, new_order: str | None) -> None:
|
||||
"""Manually set the sort order."""
|
||||
if self.sort_order == new_order or new_order is None:
|
||||
return
|
||||
|
||||
for field in self:
|
||||
field.enabled = False
|
||||
field.reversed = False
|
||||
|
||||
if new_order == "user" or new_order == "":
|
||||
self.n_enabled = 0
|
||||
self.sort_order = new_order
|
||||
else:
|
||||
position = 0
|
||||
for desc in new_order.split(", "):
|
||||
col, order = (desc.split(" ") + [None])[:2]
|
||||
if field := self.lookup(REVERSE_LOOKUP[col]):
|
||||
if not field.enabled:
|
||||
field.enabled = True
|
||||
field.reversed = order == "DESC"
|
||||
self.__move_field(field, position, update=False)
|
||||
position += 1
|
||||
self.__update_sort_order()
|
||||
|
|
|
@ -9,6 +9,12 @@ from gi.repository import Gio
|
|||
class TestSortConstants(unittest.TestCase):
|
||||
"""Test case for Sort Order constants."""
|
||||
|
||||
def test_flags(self):
|
||||
"""Test flags passed to the sort_order property."""
|
||||
self.assertEqual(emmental.tracklist.sorter.SORT_ORDER_FLAGS,
|
||||
GObject.ParamFlags.READWRITE |
|
||||
GObject.ParamFlags.EXPLICIT_NOTIFY)
|
||||
|
||||
def test_sort_fields(self):
|
||||
"""Test the sort fields definition dictionary."""
|
||||
self.assertDictEqual(emmental.tracklist.sorter.SORT_FIELDS,
|
||||
|
@ -22,6 +28,19 @@ class TestSortConstants(unittest.TestCase):
|
|||
"Title": ["title"],
|
||||
"Track Number": ["mediumno", "number"]})
|
||||
|
||||
def test_reverse_lookup(self):
|
||||
"""Test the sort field reverse lookup dictionary."""
|
||||
self.assertDictEqual(emmental.tracklist.sorter.REVERSE_LOOKUP,
|
||||
{"album": "Album", "artist": "Artist",
|
||||
"albumartist": "Album Artist",
|
||||
"filepath": "Filepath", "length": "Length",
|
||||
"lastplayed": "Last Played Date",
|
||||
"laststarted": "Last Started Date",
|
||||
"playcount": "Play Count",
|
||||
"release": "Release Date", "title": "Title",
|
||||
"mediumno": "Track Number",
|
||||
"number": "Track Number"})
|
||||
|
||||
|
||||
class TestSortField(unittest.TestCase):
|
||||
"""Test case for our SortField object."""
|
||||
|
@ -84,3 +103,197 @@ class TestSortField(unittest.TestCase):
|
|||
self.model.reverse = unittest.mock.Mock()
|
||||
self.field.reverse()
|
||||
self.model.reverse.assert_called_with(self.field)
|
||||
|
||||
|
||||
class TestSortOrderModel(unittest.TestCase):
|
||||
"""Test case for our Sort Order Model."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
self.sort_fields = emmental.tracklist.sorter.SORT_FIELDS
|
||||
self.model = emmental.tracklist.sorter.SortOrderModel()
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the model is initialized properly."""
|
||||
self.assertIsInstance(self.model, Gio.ListStore)
|
||||
self.assertEqual(self.model.n_enabled, 0)
|
||||
self.assertEqual(self.model.get_item_type(),
|
||||
emmental.tracklist.sorter.SortField.__gtype__)
|
||||
self.assertEqual(self.model.sort_order, "")
|
||||
|
||||
for i, name in enumerate(self.sort_fields.keys()):
|
||||
with self.subTest(i=i, name=name):
|
||||
self.assertIsInstance(self.model[i],
|
||||
emmental.tracklist.sorter.SortField)
|
||||
self.assertEqual(self.model[i].name, name)
|
||||
|
||||
def test_index(self):
|
||||
"""Test the Sort Order Model index() function."""
|
||||
for i, name in enumerate(emmental.tracklist.sorter.SORT_FIELDS.keys()):
|
||||
with self.subTest(i=i, name=name):
|
||||
self.assertEqual(self.model.index(self.model[i]), i)
|
||||
|
||||
field2 = emmental.tracklist.sorter.SortField(self.model, "Album")
|
||||
self.assertIsNone(self.model.index(field2))
|
||||
|
||||
def test_lookup(self):
|
||||
"""Test looping up sort fields by name."""
|
||||
for name in emmental.tracklist.sorter.SORT_FIELDS.keys():
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual(self.model.lookup(name).name, name)
|
||||
self.assertIsNone(self.model.lookup("Invalid Name"))
|
||||
|
||||
def test_disable(self):
|
||||
"""Test disabling a sort field."""
|
||||
(field_0 := self.model[0]).enable()
|
||||
(field_1 := self.model[1]).enable()
|
||||
(field_2 := self.model[2]).enable()
|
||||
field_2.reversed = True
|
||||
|
||||
self.assertTrue(self.model.disable(field_2))
|
||||
self.assertFalse(field_2.enabled)
|
||||
self.assertFalse(field_2.reversed)
|
||||
self.assertEqual(self.model.n_enabled, 2)
|
||||
self.assertEqual(self.model.index(field_2), 2)
|
||||
self.assertEqual(self.model.sort_order,
|
||||
f"{str(field_0)}, {str(field_1)}")
|
||||
|
||||
self.assertFalse(self.model.disable(field_2))
|
||||
self.assertEqual(self.model.n_enabled, 2)
|
||||
|
||||
self.assertTrue(self.model.disable(field_0))
|
||||
self.assertFalse(field_0.enabled)
|
||||
self.assertEqual(self.model.n_enabled, 1)
|
||||
self.assertEqual(self.model.index(field_0), 1)
|
||||
self.assertEqual(self.model.sort_order, str(field_1))
|
||||
|
||||
def test_enable(self):
|
||||
"""Test enabling a sort field."""
|
||||
field_0 = self.model[0]
|
||||
self.assertTrue(self.model.enable(field_0))
|
||||
self.assertTrue(field_0.enabled)
|
||||
self.assertEqual(self.model.n_enabled, 1)
|
||||
self.assertEqual(self.model.index(field_0), 0)
|
||||
self.assertEqual(self.model.sort_order, str(field_0))
|
||||
|
||||
self.assertFalse(self.model.enable(field_0))
|
||||
self.assertEqual(self.model.n_enabled, 1)
|
||||
|
||||
field_3 = self.model[3]
|
||||
self.assertTrue(self.model.enable(field_3))
|
||||
self.assertTrue(field_3.enabled)
|
||||
self.assertEqual(self.model.n_enabled, 2)
|
||||
self.assertEqual(self.model.index(field_3), 1)
|
||||
self.assertEqual(self.model.sort_order,
|
||||
f"{str(field_0)}, {str(field_3)}")
|
||||
|
||||
def test_move_down(self):
|
||||
"""Test moving a sort field down in the list."""
|
||||
(field_0 := self.model[0]).enable()
|
||||
(field_1 := self.model[1]).enable()
|
||||
|
||||
for res in [True, False]:
|
||||
with self.subTest(res=res):
|
||||
self.assertEqual(self.model.move_down(field_0), res)
|
||||
self.assertEqual(self.model[0], field_1)
|
||||
self.assertEqual(self.model[1], field_0)
|
||||
self.assertEqual(self.model.sort_order,
|
||||
f"{str(field_1)}, {str(field_0)}")
|
||||
|
||||
def test_move_up(self):
|
||||
"""Test moving a sort field down in the list."""
|
||||
(field_0 := self.model[0]).enable()
|
||||
(field_1 := self.model[1]).enable()
|
||||
|
||||
for res in [True, False]:
|
||||
with self.subTest(res=res):
|
||||
self.assertEqual(self.model.move_up(field_1), res)
|
||||
self.assertEqual(self.model[0], field_1)
|
||||
self.assertEqual(self.model[1], field_0)
|
||||
self.assertEqual(self.model.sort_order,
|
||||
f"{str(field_1)}, {str(field_0)}")
|
||||
|
||||
def test_reverse(self):
|
||||
"""Test reversing a sort field."""
|
||||
(field_0 := self.model[0]).enable()
|
||||
self.model.reverse(field_0)
|
||||
self.assertTrue(field_0.reversed)
|
||||
self.assertEqual(self.model.sort_order, str(field_0))
|
||||
|
||||
self.model.reverse(field_0)
|
||||
self.assertFalse(field_0.reversed)
|
||||
self.assertEqual(self.model.sort_order, str(field_0))
|
||||
|
||||
def test_set_sort_order_artist(self):
|
||||
"""Test setting the Sort Order property to "artist"."""
|
||||
self.model.set_sort_order("artist")
|
||||
self.assertEqual(self.model.n_enabled, 1)
|
||||
self.assertEqual(self.model.sort_order, "artist")
|
||||
|
||||
self.assertEqual(self.model[0].name, "Artist")
|
||||
self.assertTrue(self.model[0].enabled)
|
||||
self.assertFalse(self.model[0].reversed)
|
||||
|
||||
self.assertFalse(self.model[1].enabled)
|
||||
|
||||
def test_set_sort_order_album_title(self):
|
||||
"""Test setting the Sort Order property to "album DESC, title"."""
|
||||
self.model[1].enable()
|
||||
self.model[1].reverse()
|
||||
|
||||
self.model.set_sort_order("album DESC, title")
|
||||
self.assertEqual(self.model.n_enabled, 2)
|
||||
self.assertEqual(self.model.sort_order, "album DESC, title")
|
||||
|
||||
self.assertEqual(self.model[0].name, "Album")
|
||||
self.assertTrue(self.model[0].enabled)
|
||||
self.assertTrue(self.model[0].reversed)
|
||||
|
||||
self.assertEqual(self.model[1].name, "Title")
|
||||
self.assertTrue(self.model[1].enabled)
|
||||
self.assertFalse(self.model[1].reversed)
|
||||
|
||||
self.assertFalse(self.model[2].enabled)
|
||||
self.assertFalse(self.model[2].reversed)
|
||||
|
||||
def test_set_sort_order_trackno_playcount(self):
|
||||
"""Test setting the Sort Order property to "album DESC, title"."""
|
||||
self.model.set_sort_order("mediumno, number, playcount")
|
||||
self.assertEqual(self.model.n_enabled, 2)
|
||||
self.assertEqual(self.model.sort_order, "mediumno, number, playcount")
|
||||
|
||||
self.assertEqual(self.model[0].name, "Track Number")
|
||||
self.assertTrue(self.model[0].enabled)
|
||||
self.assertFalse(self.model[0].reversed)
|
||||
|
||||
self.assertEqual(self.model[1].name, "Play Count")
|
||||
self.assertTrue(self.model[1].enabled)
|
||||
self.assertFalse(self.model[1].reversed)
|
||||
|
||||
self.assertFalse(self.model[2].enabled)
|
||||
self.assertFalse(self.model[2].reversed)
|
||||
|
||||
def test_set_sort_order_user(self):
|
||||
"""Test setting the Sort Order property to "user"."""
|
||||
self.model[0].enable()
|
||||
|
||||
self.model.set_sort_order("user")
|
||||
self.assertEqual(self.model.n_enabled, 0)
|
||||
self.assertEqual(self.model.sort_order, "user")
|
||||
self.assertFalse(self.model[0].enabled)
|
||||
|
||||
def test_set_sort_order_same(self):
|
||||
"""Test setting the Sort Order property to the same value."""
|
||||
self.model.set_sort_order("user")
|
||||
|
||||
notify = unittest.mock.Mock()
|
||||
self.model.connect("notify::sort-order", notify)
|
||||
self.model.set_sort_order("user")
|
||||
notify.assert_not_called()
|
||||
|
||||
def test_set_sort_order_none(self):
|
||||
"""Test setting the Sort Order property to 'None'."""
|
||||
notify = unittest.mock.Mock()
|
||||
self.model.connect("notify::sort-order", notify)
|
||||
self.model.set_sort_order(None)
|
||||
notify.assert_not_called()
|
||||
|
|
Loading…
Reference in New Issue