diff --git a/emmental/tracklist/buttons.py b/emmental/tracklist/buttons.py index a0daff4..7452808 100644 --- a/emmental/tracklist/buttons.py +++ b/emmental/tracklist/buttons.py @@ -5,7 +5,6 @@ from gi.repository import Gio from gi.repository import Gtk from . import sorter from .. import buttons -from .. import factory class VisibleRow(Gtk.ListBoxRow): @@ -111,81 +110,57 @@ class ShuffleButton(buttons.ImageToggle): 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.""" +class SortRow(Gtk.ListBoxRow): + """A ListBoxRow for managing Sort Order.""" + active = GObject.Property(type=bool, default=False) 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) + def __init__(self, sort_field: sorter.SortField): + """Initialize a Sort Row.""" + super().__init__(sort_field=sort_field, active=sort_field.enabled, + child=Gtk.Box(margin_start=6, margin_end=6, + margin_top=6, margin_bottom=6, + spacing=6)) + self._switch = Gtk.Switch(active=self.active, valign=Gtk.Align.CENTER) + self._label = Gtk.Label(label=sort_field.name, hexpand=True, + sensitive=self.active, xalign=0.0) self._reverse = buttons.ImageToggle("arrow1-up", "arrow1-down", - large_icon=False, sensitive=False) - self._box = Gtk.Box(sensitive=False) + active=sort_field.reversed, + sensitive=self.active, + has_frame=False) + self._move_box = Gtk.Box(sensitive=self.active) 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._switch.connect("notify::active", self.__toggle_enabled) + self._reverse.connect("toggled", self.__reverse) + self._move_up.connect("clicked", self.__move_up) + self._move_down.connect("clicked", self.__move_down) - 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.props.child.append(self._switch) + self.props.child.append(self._label) + self.props.child.append(self._reverse) + self.props.child.append(self._move_box) - self.append(self._enabled) - self.append(self._name) - self.append(self._reverse) - self.append(self._box) + self._move_box.append(self._move_up) + self._move_box.append(self._move_down) + self._move_box.add_css_class("linked") - 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 __toggle_enabled(self, switch: Gtk.Switch, param) -> None: + if switch.props.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() + 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 + def __move_up(self, button: Gtk.Button) -> None: + self.sort_field.move_up() - -class SortRow(factory.ListRow): - """A row for managing Sort Order.""" - - def __init__(self, listitem: Gtk.ListItem): - """Initialize a Sort Row.""" - super().__init__(listitem=listitem, child=SortFieldWidget()) - - def do_bind(self) -> None: - """Bind Sort Field properties to the Widget.""" - self.child.set_sort_field(self.item) - - def do_unbind(self) -> None: - """Unbind properties from the widget.""" - self.child.set_sort_field(None) + def __move_down(self, button: Gtk.Button) -> None: + self.sort_field.move_down() class SortButton(buttons.PopoverButton): @@ -198,13 +173,22 @@ class SortButton(buttons.PopoverButton): """Initialize the Sort button.""" super().__init__(has_frame=False, model=sorter.SortOrderModel(), icon_name="view-list-ordered-symbolic", **kwargs) - self._selection = Gtk.NoSelection(model=self.model) - self._factory = factory.Factory(row_type=SortRow) - self.popover_child = Gtk.ListView(model=self._selection, - factory=self._factory, - show_separators=True) + self.popover_child = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) + self.popover_child.bind_model(self.model, self.__create_func) + self.popover_child.connect("row-activated", self.__row_activated) + self.popover_child.add_css_class("boxed-list") + self.model.bind_property("sort-order", self, "sort-order") + def __create_func(self, sort_field: sorter.SortField) -> SortRow: + return SortRow(sort_field) + + def __row_activated(self, box: Gtk.ListBox, row: SortRow) -> None: + if row.active: + row._reverse.active = not row.sort_field.reversed + else: + row.sort_field.enable() + def set_sort_order(self, newval: str) -> None: """Directly set the sort order.""" self.model.set_sort_order(newval) diff --git a/tests/tracklist/test_buttons.py b/tests/tracklist/test_buttons.py index fd85573..e1b0d0b 100644 --- a/tests/tracklist/test_buttons.py +++ b/tests/tracklist/test_buttons.py @@ -195,123 +195,123 @@ class TestShuffleButtons(unittest.TestCase): self.assertEqual(self.shuffle.icon_opacity, 0.5) -class TestSortFieldWidget(unittest.TestCase): - """Test the Sort Field widget.""" +class TestSortRow(unittest.TestCase): + """Test the Sort Row ListBoxRow.""" 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() + self.model[1].reverse() + self.row1 = emmental.tracklist.buttons.SortRow(self.model[0]) + self.row2 = emmental.tracklist.buttons.SortRow(self.model[1]) 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.assertIsInstance(self.row1, Gtk.ListBoxRow) + self.assertIsInstance(self.row1.props.child, Gtk.Box) - self.assertTrue(self.sort._name.get_hexpand()) + self.assertEqual(self.row1.props.child.props.margin_start, 6) + self.assertEqual(self.row1.props.child.props.margin_end, 6) + self.assertEqual(self.row1.props.child.props.margin_top, 6) + self.assertEqual(self.row1.props.child.props.margin_bottom, 6) + self.assertEqual(self.row1.props.child.props.spacing, 6) - 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")) + self.assertEqual(self.row1.sort_field, self.model[0]) + self.assertTrue(self.row1.active) - def test_set_sort_field(self): - """Test setting a sort field to the Sort Field Widget.""" - self.assertIsNone(self.sort.sort_field) + self.assertEqual(self.row2.sort_field, self.model[1]) + self.assertFalse(self.row2.active) - 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) + def test_switch(self): + """Test the SortRow switch.""" + self.assertIsInstance(self.row1._switch, Gtk.Switch) + self.assertEqual(self.row1._switch.props.valign, Gtk.Align.CENTER) + self.assertEqual(self.row1._switch.props.parent, self.row1.props.child) - 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) + self.assertTrue(self.row1._switch.props.active) + self.assertFalse(self.row2._switch.props.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) + with unittest.mock.patch.object(self.model[0], + "disable") as mock_disable: + self.row1._switch.props.active = False + mock_disable.assert_called() - self.sort._enabled.set_active(True) + with unittest.mock.patch.object(self.model[0], + "enable") as mock_enable: + self.row1._switch.props.active = True + mock_enable.assert_called() - 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()) + def test_label(self): + """Test the SortRow title label.""" + self.assertIsInstance(self.row1._label, Gtk.Label) + self.assertEqual(self.row1._switch.get_next_sibling(), + self.row1._label) - 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.assertEqual(self.row1._label.props.label, self.model[0].name) + self.assertEqual(self.row1._label.props.xalign, 0.0) + self.assertTrue(self.row1._label.props.hexpand) - 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) + self.assertTrue(self.row1._label.props.sensitive) + self.assertFalse(self.row2._label.props.sensitive) def test_reverse(self): - """Test reversing a sort field.""" - self.assertIsInstance(self.sort._reverse, emmental.buttons.ImageToggle) - self.assertEqual(self.sort._name.get_next_sibling(), - self.sort._reverse) + """Test the SortRow reverse button.""" + self.assertIsInstance(self.row1._reverse, emmental.buttons.ImageToggle) + self.assertEqual(self.row1._label.get_next_sibling(), + self.row1._reverse) - self.assertEqual(self.sort._reverse.active_icon_name, "arrow1-up") - self.assertEqual(self.sort._reverse.inactive_icon_name, "arrow1-down") - self.assertFalse(self.sort._reverse.large_icon) + self.assertEqual(self.row1._reverse.active_icon_name, "arrow1-up") + self.assertEqual(self.row1._reverse.inactive_icon_name, "arrow1-down") + self.assertFalse(self.row1._reverse.props.has_frame) + self.assertFalse(self.row1._reverse.large_icon) - self.sort._reverse.emit("clicked") + self.assertFalse(self.row1._reverse.props.active) + self.assertTrue(self.row1._reverse.props.sensitive) - 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) + self.assertTrue(self.row2._reverse.props.active) + self.assertFalse(self.row2._reverse.props.sensitive) + + with unittest.mock.patch.object(self.model[0], + "reverse") as mock_reverse: + self.row1._reverse.emit("toggled") + mock_reverse.assert_called() + + def test_move_box(self): + """Test the box containing the move up & down buttons.""" + self.assertIsInstance(self.row1._move_box, Gtk.Box) + self.assertEqual(self.row1._reverse.get_next_sibling(), + self.row1._move_box) + self.assertTrue(self.row1._move_box.has_css_class("linked")) + + self.assertTrue(self.row1._move_box.props.sensitive) + self.assertFalse(self.row2._move_box.props.sensitive) + + def test_move_up(self): + """Test the move up button.""" + self.assertIsInstance(self.row1._move_up, Gtk.Button) + self.assertEqual(self.row1._move_up.get_icon_name(), + "go-up-symbolic") + self.assertEqual(self.row1._move_up.props.parent, + self.row1._move_box) + + with unittest.mock.patch.object(self.model[0], + "move_up") as mock_move_up: + self.row1._move_up.emit("clicked") + mock_move_up.assert_called() + + def test_move_down(self): + """Test the move down button.""" + self.assertIsInstance(self.row1._move_down, Gtk.Button) + self.assertEqual(self.row1._move_down.get_icon_name(), + "go-down-symbolic") + self.assertEqual(self.row1._move_up.get_next_sibling(), + self.row1._move_down) + + with unittest.mock.patch.object(self.model[0], + "move_down") as mock_move_down: + self.row1._move_down.emit("clicked") + mock_move_down.assert_called() class TestSortButton(unittest.TestCase): @@ -331,38 +331,36 @@ class TestSortButton(unittest.TestCase): def test_popover_child(self): """Test that the popover_child is configured correctly.""" - self.assertIsInstance(self.sort.popover_child, Gtk.ListView) - self.assertIsInstance(self.sort.model, - emmental.tracklist.sorter.SortOrderModel) - self.assertIsInstance(self.sort._selection, Gtk.NoSelection) - self.assertIsInstance(self.sort._factory, emmental.factory.Factory) + self.assertIsInstance(self.sort.popover_child, Gtk.ListBox) + self.assertEqual(self.sort.popover_child.props.selection_mode, + Gtk.SelectionMode.NONE) + self.assertTrue(self.sort.popover_child.has_css_class("boxed-list")) - self.assertTrue(self.sort.popover_child.get_show_separators()) + def test_create_func(self): + """Test that the Gtk.ListBox creates SortRows correctly.""" + row = self.sort.popover_child.get_row_at_index(0) + self.assertIsInstance(row, emmental.tracklist.buttons.SortRow) + self.assertEqual(row.sort_field, self.sort.model[0]) - self.assertEqual(self.sort.popover_child.get_model(), - self.sort._selection) - self.assertEqual(self.sort._selection.get_model(), self.sort.model) - self.assertEqual(self.sort.popover_child.get_factory(), - self.sort._factory) - self.assertEqual(self.sort._factory.row_type, - emmental.tracklist.buttons.SortRow) + def test_activate(self): + """Test activating a Gtk.ListBox sort row.""" + row = self.sort.popover_child.get_row_at_index(0) + field = row.sort_field + self.assertFalse(field.enabled) + self.assertFalse(field.reversed) - def test_sort_row(self): - """Test the Sort Row object.""" - (field := self.sort.model[0]).enable() - listitem = Gtk.ListItem() - listitem.get_item = lambda: field - self.sort.model[1].enable() + with unittest.mock.patch.object(field, "enable") as mock_enable: + self.sort.popover_child.emit("row-activated", row) + mock_enable.assert_called() - row = emmental.tracklist.buttons.SortRow(listitem) - self.assertIsInstance(row, emmental.factory.ListRow) - self.assertIsInstance(row.child, - emmental.tracklist.buttons.SortFieldWidget) + mock_enable.reset_mock() + row.active = True - row.bind() - self.assertEqual(row.child.sort_field, field) - row.unbind() - self.assertIsNone(row.child.sort_field) + with unittest.mock.patch.object(field, "reverse") as mock_reverse: + self.sort.popover_child.emit("row-activated", row) + self.assertTrue(row._reverse.active) + mock_enable.assert_not_called() + mock_reverse.assert_called() def test_sort_order(self): """Test the sort-order property."""