diff --git a/emmental/tracklist/buttons.py b/emmental/tracklist/buttons.py
index 852e278..e3d49e1 100644
--- a/emmental/tracklist/buttons.py
+++ b/emmental/tracklist/buttons.py
@@ -3,6 +3,7 @@
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
+from . import sorter
from .. import buttons
from .. import factory
@@ -99,3 +100,65 @@ class ShuffleButton(buttons.ImageToggle):
def do_toggled(self):
"""Adjust opacity when active state toggles."""
self.icon_opacity = 1.0 if self.active else 0.5
+
+
+class SortFieldWidget(Gtk.Box):
+ """A Widget to display in the Sort Order button popover."""
+
+ sort_field = GObject.Property(type=sorter.SortField)
+
+ def __init__(self) -> None:
+ """Initialize a SortField Widget."""
+ super().__init__(spacing=6)
+ self._enabled = Gtk.Switch(valign=Gtk.Align.CENTER)
+ self._name = Gtk.Label(hexpand=True, sensitive=False)
+ self._reverse = buttons.ImageToggle("arrow1-up", "arrow1-down",
+ icon_size=Gtk.IconSize.NORMAL,
+ sensitive=False)
+ self._box = Gtk.Box(sensitive=False)
+ self._move_up = Gtk.Button(icon_name="go-up-symbolic")
+ self._move_down = Gtk.Button(icon_name="go-down-symbolic")
+
+ self._enabled.bind_property("active", self._name, "sensitive")
+ self._enabled.bind_property("active", self._reverse, "sensitive")
+ self._enabled.bind_property("active", self._box, "sensitive")
+
+ self._enabled.connect("notify::active", self.__notify_enabled)
+ self._reverse.connect("clicked", self.__reverse)
+ self._move_up.connect("clicked", self.__move_item_up)
+ self._move_down.connect("clicked", self.__move_item_down)
+
+ self.append(self._enabled)
+ self.append(self._name)
+ self.append(self._reverse)
+ self.append(self._box)
+
+ self._box.append(self._move_up)
+ self._box.append(self._move_down)
+ self._box.add_css_class("linked")
+
+ def __move_item_down(self, button: Gtk.Button) -> None:
+ if self.sort_field is not None:
+ self.sort_field.move_down()
+
+ def __move_item_up(self, button: Gtk.Button) -> None:
+ if self.sort_field is not None:
+ self.sort_field.move_up()
+
+ def __notify_enabled(self, switch: Gtk.Switch, param) -> None:
+ if self.sort_field is not None:
+ if switch.get_active():
+ self.sort_field.enable()
+ else:
+ self.sort_field.disable()
+
+ def __reverse(self, button: buttons.ImageToggle) -> None:
+ if self.sort_field is not None:
+ self.sort_field.reverse()
+
+ def set_sort_field(self, field: sorter.SortField | None) -> None:
+ """Set the Sort Field displayed by this Widget."""
+ self.sort_field = field
+ self._name.set_text(field.name if field is not None else "")
+ self._enabled.set_active(field is not None and field.enabled)
+ self._reverse.active = field is not None and field.reversed
diff --git a/icons/scalable/actions/arrow1-down-symbolic.svg b/icons/scalable/actions/arrow1-down-symbolic.svg
new file mode 100644
index 0000000..fc69237
--- /dev/null
+++ b/icons/scalable/actions/arrow1-down-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/icons/scalable/actions/arrow1-up-symbolic.svg b/icons/scalable/actions/arrow1-up-symbolic.svg
new file mode 100644
index 0000000..fbff73b
--- /dev/null
+++ b/icons/scalable/actions/arrow1-up-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/tests/tracklist/test_buttons.py b/tests/tracklist/test_buttons.py
index 68645cc..4ad47b1 100644
--- a/tests/tracklist/test_buttons.py
+++ b/tests/tracklist/test_buttons.py
@@ -157,3 +157,121 @@ class TestShuffleButtons(unittest.TestCase):
self.assertEqual(self.shuffle.icon_opacity, 1.0)
self.shuffle.active = False
self.assertEqual(self.shuffle.icon_opacity, 0.5)
+
+
+class TestSortFieldWidget(unittest.TestCase):
+ """Test the Sort Field widget."""
+
+ def setUp(self):
+ """Set up common variables."""
+ self.sort = emmental.tracklist.buttons.SortFieldWidget()
+ self.model = emmental.tracklist.sorter.SortOrderModel()
+ self.model[0].enable()
+ self.model[0].reverse()
+
+ def test_init(self):
+ """Test that the Sort Field Widget is configured correctly."""
+ self.assertIsInstance(self.sort, Gtk.Box)
+ self.assertIsInstance(self.sort._box, Gtk.Box)
+ self.assertIsInstance(self.sort._name, Gtk.Label)
+
+ self.assertTrue(self.sort._name.get_hexpand())
+
+ self.assertEqual(self.sort.get_spacing(), 6)
+ self.assertEqual(self.sort._enabled.get_next_sibling(),
+ self.sort._name)
+ self.assertEqual(self.sort._reverse.get_next_sibling(),
+ self.sort._box)
+ self.assertTrue(self.sort._box.has_css_class("linked"))
+
+ def test_set_sort_field(self):
+ """Test setting a sort field to the Sort Field Widget."""
+ self.assertIsNone(self.sort.sort_field)
+
+ self.sort.set_sort_field(self.model[0])
+ self.assertEqual(self.sort.sort_field, self.model[0])
+ self.assertEqual(self.sort._name.get_text(), self.model[0].name)
+ self.assertTrue(self.sort._enabled.get_active())
+ self.assertTrue(self.sort._reverse.active)
+
+ self.sort.set_sort_field(None)
+ self.assertIsNone(self.sort.sort_field)
+ self.assertEqual(self.sort._name.get_text(), "")
+ self.assertFalse(self.sort._enabled.get_active())
+ self.assertFalse(self.sort._reverse.active)
+
+ def test_enabled(self):
+ """Test enabling and disabling a sort field."""
+ self.assertIsInstance(self.sort._enabled, Gtk.Switch)
+ self.assertEqual(self.sort._enabled.get_valign(), Gtk.Align.CENTER)
+ self.assertEqual(self.sort.get_first_child(), self.sort._enabled)
+
+ self.sort._enabled.set_active(True)
+
+ self.sort.set_sort_field(self.model[1])
+ self.assertFalse(self.sort._name.get_sensitive())
+ self.assertFalse(self.sort._box.get_sensitive())
+ self.assertFalse(self.sort._reverse.get_sensitive())
+
+ self.sort._enabled.set_active(True)
+ self.assertTrue(self.model[1].enabled)
+ self.assertTrue(self.sort._name.get_sensitive())
+ self.assertTrue(self.sort._box.get_sensitive())
+ self.assertTrue(self.sort._reverse.get_sensitive())
+
+ self.sort._enabled.set_active(False)
+ self.assertFalse(self.model[1].enabled)
+ self.assertFalse(self.sort._name.get_sensitive())
+ self.assertFalse(self.sort._box.get_sensitive())
+ self.assertFalse(self.sort._reverse.get_sensitive())
+
+ def test_move_down(self):
+ """Test the moving a sort field down."""
+ self.assertIsInstance(self.sort._move_down, Gtk.Button)
+ self.assertEqual(self.sort._move_down.get_icon_name(),
+ "go-down-symbolic")
+ self.assertEqual(self.sort._move_up.get_next_sibling(),
+ self.sort._move_down)
+
+ self.sort._move_down.emit("clicked")
+
+ (field := self.model[0]).enable()
+ self.model[1].enable()
+ self.sort.set_sort_field(field)
+
+ self.sort._move_down.emit("clicked")
+ self.assertEqual(self.model.index(field), 1)
+
+ def test_move_up(self):
+ """Test the moving a sort field."""
+ self.assertIsInstance(self.sort._move_up, Gtk.Button)
+ self.assertEqual(self.sort._move_up.get_icon_name(), "go-up-symbolic")
+
+ self.assertEqual(self.sort._box.get_first_child(),
+ self.sort._move_up)
+
+ self.sort._move_up.emit("clicked")
+
+ self.model[0].enable()
+ (field := self.model[1]).enable()
+ self.sort.set_sort_field(field)
+
+ self.sort._move_up.emit("clicked")
+ self.assertEqual(self.model.index(field), 0)
+
+ def test_reverse(self):
+ """Test reversing a sort field."""
+ self.assertIsInstance(self.sort._reverse, emmental.buttons.ImageToggle)
+ self.assertEqual(self.sort._reverse.active_icon_name, "arrow1-up")
+ self.assertEqual(self.sort._reverse.inactive_icon_name, "arrow1-down")
+ self.assertEqual(self.sort._reverse.icon_size, Gtk.IconSize.NORMAL)
+ self.assertEqual(self.sort._name.get_next_sibling(),
+ self.sort._reverse)
+
+ self.sort._reverse.emit("clicked")
+
+ self.sort.set_sort_field(self.model[0])
+ self.sort._reverse.emit("clicked")
+ self.assertFalse(self.model[0].reversed)
+ self.sort._reverse.emit("clicked")
+ self.assertTrue(self.model[0].reversed)