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)