From 5011db344e5b91fa8c1f4e00d005157c3d46eeb3 Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Tue, 22 Aug 2023 15:14:35 -0400 Subject: [PATCH] tracklist: Rework the Add Tracks to Playlist button to use a ListBox I also convert my PlaylistRowWidget into a Gtk.ListBoxRow that has the same functionality. This looks a little nicer, and lets us keep the same style as the rest of the app. Signed-off-by: Anna Schumaker --- emmental/tracklist/selection.py | 76 +++++++--------- tests/tracklist/test_selection.py | 145 ++++++++++++++---------------- 2 files changed, 95 insertions(+), 126 deletions(-) diff --git a/emmental/tracklist/selection.py b/emmental/tracklist/selection.py index 6a9897c..59880d3 100644 --- a/emmental/tracklist/selection.py +++ b/emmental/tracklist/selection.py @@ -6,38 +6,36 @@ from gi.repository import Gtk from gi.repository import Adw from ..buttons import PopoverButton from .. import db -from .. import factory from .. import playlist -class PlaylistRowWidget(Gtk.Box): - """A row widget for Playlists.""" +class PlaylistRow(Gtk.ListBoxRow): + """A ListBoxRow widget for Playlists.""" name = GObject.Property(type=str) image = GObject.Property(type=GObject.TYPE_PYOBJECT) - def __init__(self): - """Initialize a PlaylistRowWidget.""" - super().__init__() - self._icon = Adw.Avatar(size=32) - self._label = Gtk.Label(xalign=0.0) + def __init__(self, name: str, image: GObject.TYPE_PYOBJECT): + """Initialize a PlaylistRow.""" + super().__init__(child=Gtk.Box(margin_start=6, margin_end=6, + margin_top=6, margin_bottom=6, + spacing=6), name=name) + match name: + case "Favorite Tracks": icon_name = "heart-filled-symbolic" + case "Queued Tracks": icon_name = "music-queue-symbolic" + case _: icon_name = "playlist2-symbolic" + + self._icon = Adw.Avatar(size=32, text=name, icon_name=icon_name) + self._label = Gtk.Label.new(name) - self.bind_property("name", self._label, "label") - self.bind_property("name", self._icon, "text") - self.connect("notify::name", self.__name_changed) self.connect("notify::image", self.__image_changed) + self.image = image - self.append(self._icon) - self.append(self._label) + self.props.child.append(self._icon) + self.props.child.append(self._label) - def __name_changed(self, row: Gtk.Box, param) -> None: - match self.name: - case "Favorite Tracks": icon = "heart-filled-symbolic" - case "Queued Tracks": icon = "music-queue-symbolic" - case _: icon = "playlist2-symbolic" - self._icon.set_icon_name(icon) - - def __image_changed(self, row: Gtk.Box, param) -> None: + def __image_changed(self, row: Gtk.ListBoxRow, + param: GObject.ParamSpec) -> None: if self.image is not None and self.image.is_file(): texture = Gdk.Texture.new_from_filename(str(self.image)) else: @@ -45,20 +43,6 @@ class PlaylistRowWidget(Gtk.Box): self._icon.set_custom_image(texture) -class PlaylistRow(factory.ListRow): - """A list row for displaying Playlists.""" - - def __init__(self, listitem: Gtk.ListItem): - """Initialize a PlaylistRow.""" - super().__init__(listitem) - self.child = PlaylistRowWidget() - - def do_bind(self): - """Bind a Playlist to this Row.""" - self.bind_and_set_property("name", "name") - self.bind_and_set_property("image", "image") - - class UserTracksFilter(Gtk.Filter): """Filters for tracks with user-tracks set to True.""" @@ -77,28 +61,28 @@ class UserTracksFilter(Gtk.Filter): return playlist.user_tracks and playlist != self.playlist -class PlaylistView(Gtk.ListView): +class PlaylistView(Gtk.ListBox): """A ListView for selecting Playlists.""" playlist = GObject.Property(type=db.playlist.Playlist) def __init__(self, sql: db.Connection): """Initialize the PlaylistView.""" - super().__init__(show_separators=True, single_click_activate=True) + super().__init__(selection_mode=Gtk.SelectionMode.NONE) self._filtered = Gtk.FilterListModel(model=sql.playlists, filter=UserTracksFilter()) - self._selection = Gtk.NoSelection(model=self._filtered) - self._factory = factory.Factory(PlaylistRow) - - self.connect("activate", self.__playlist_activated) self.bind_property("playlist", self._filtered.get_filter(), "playlist") - self.add_css_class("rich-list") + self.bind_model(self._filtered, self.__create_func) + self.connect("row-activated", self.__row_activated) + self.add_css_class("boxed-list") - self.set_model(self._selection) - self.set_factory(self._factory) + def __row_activated(self, box: Gtk.ListBox, row: PlaylistRow) -> None: + self.emit("playlist-selected", self._filtered[row.get_index()]) - def __playlist_activated(self, view: Gtk.ListView, position: int) -> None: - self.emit("playlist-selected", self._selection[position]) + def __create_func(self, playlist: db.playlist.Playlist) -> PlaylistRow: + row = PlaylistRow(playlist.name, playlist.image) + playlist.bind_property("image", row, "image") + return row @GObject.Signal(arg_types=(db.playlists.Playlist,)) def playlist_selected(self, playlist: db.playlists.Playlist) -> None: diff --git a/tests/tracklist/test_selection.py b/tests/tracklist/test_selection.py index 74da5b8..329b523 100644 --- a/tests/tracklist/test_selection.py +++ b/tests/tracklist/test_selection.py @@ -10,83 +10,65 @@ from gi.repository import Gtk from gi.repository import Adw -class TestPlaylistRowWidget(unittest.TestCase): - """Test the Playlist Row Widget.""" +class TestPlaylistRow(unittest.TestCase): + """Test the PlaylistRow Widget.""" def setUp(self): """Set up common variables.""" - self.widget = emmental.tracklist.selection.PlaylistRowWidget() + cover = tests.util.COVER_JPG + self.row = emmental.tracklist.selection.PlaylistRow("name", cover) def test_init(self): - """Test that the Playlist Row Widget is set up properly.""" - self.assertIsInstance(self.widget, Gtk.Box) + """Test that the PlaylistRow Widget is set up properly.""" + self.assertIsInstance(self.row, Gtk.ListBoxRow) + self.assertIsInstance(self.row.props.child, Gtk.Box) + + self.assertEqual(self.row.name, "name") + self.assertEqual(self.row.image, tests.util.COVER_JPG) + + self.assertEqual(self.row.props.child.props.margin_start, 6) + self.assertEqual(self.row.props.child.props.margin_end, 6) + self.assertEqual(self.row.props.child.props.margin_top, 6) + self.assertEqual(self.row.props.child.props.margin_bottom, 6) + self.assertEqual(self.row.props.child.props.spacing, 6) def test_label(self): - """Test the Playlist Row Widget label.""" - self.widget.name = "Test Playlist Name" - self.assertIsInstance(self.widget._label, Gtk.Label) - self.assertEqual(self.widget._label.get_text(), "Test Playlist Name") - self.assertEqual(self.widget._label.get_xalign(), 0.0) - self.assertEqual(self.widget._icon.get_next_sibling(), - self.widget._label) + """Test the PlaylistRow Widget label.""" + self.assertIsInstance(self.row._label, Gtk.Label) + self.assertEqual(self.row._label.props.label, "name") + self.assertEqual(self.row._icon.get_next_sibling(), self.row._label) def test_icon(self): - """Test the Playlist Row Widget icon.""" - self.assertIsInstance(self.widget._icon, Adw.Avatar) - self.assertEqual(self.widget.get_first_child(), self.widget._icon) - self.assertEqual(self.widget._icon.get_size(), 32) - - self.widget.name = "Favorite Tracks" - self.assertEqual(self.widget._icon.get_icon_name(), - "heart-filled-symbolic") - self.assertEqual(self.widget._icon.get_text(), "Favorite Tracks") - - self.widget.name = "Queued Tracks" - self.assertEqual(self.widget._icon.get_icon_name(), - "music-queue-symbolic") - self.assertEqual(self.widget._icon.get_text(), "Queued Tracks") - - self.widget.name = "Any Other Name" - self.assertEqual(self.widget._icon.get_icon_name(), + """Test the PlaylistRow Widget icon.""" + self.assertIsInstance(self.row._icon, Adw.Avatar) + self.assertEqual(self.row.props.child.get_first_child(), + self.row._icon) + self.assertEqual(self.row._icon.get_size(), 32) + self.assertEqual(self.row._icon.get_text(), "name") + self.assertEqual(self.row._icon.get_icon_name(), "playlist2-symbolic") - self.assertEqual(self.widget._icon.get_text(), "Any Other Name") + + fav = emmental.tracklist.selection.PlaylistRow("Favorite Tracks", None) + self.assertEqual(fav._icon.props.icon_name, "heart-filled-symbolic") + + queue = emmental.tracklist.selection.PlaylistRow("Queued Tracks", None) + self.assertEqual(queue._icon.props.icon_name, "music-queue-symbolic") def test_image(self): - """Test the Playlist Row Widget image.""" - self.assertIsNone(self.widget.image) - self.widget.image = tests.util.COVER_JPG - self.assertIsNotNone(self.widget._icon.get_custom_image()) - self.widget.image = None - self.assertIsNone(self.widget._icon.get_custom_image()) - self.widget.image = pathlib.Path("/a/b/c.jpg") - self.assertIsNone(self.widget._icon.get_custom_image()) + """Test the PlaylistRow Widget image.""" + self.assertIsNotNone(self.row._icon.props.custom_image) + none = emmental.tracklist.selection.PlaylistRow("none", None) + self.assertIsNone(none.image) + self.assertIsNone(none._icon.props.custom_image) -class TestPlaylistRow(tests.util.TestCase): - """Test the PlaylistRow widget.""" + later = emmental.tracklist.selection.PlaylistRow("later", None) + later.image = tests.util.COVER_JPG + self.assertIsNotNone(later._icon.props.custom_image) - def setUp(self): - """Set up common variables.""" - super().setUp() - self.playlist = self.sql.playlists.create("Test Playlist") - self.listitem = Gtk.ListItem() - self.listitem.get_item = unittest.mock.Mock(return_value=self.playlist) - self.row = emmental.tracklist.selection.PlaylistRow(self.listitem) - - def test_init(self): - """Test that the PlaylistRow is initialized properly.""" - self.assertIsInstance(self.row, emmental.factory.ListRow) - self.assertIsInstance(self.row.child, - emmental.tracklist.selection.PlaylistRowWidget) - - def test_bind(self): - """Test binding the PlaylistRow.""" - self.row.bind() - self.assertEqual(self.row.child.name, "Test Playlist") - self.assertIsNone(self.row.child.image) - - self.playlist.image = pathlib.Path("/a/b/c.jpg") - self.assertEqual(self.row.child.image, pathlib.Path("/a/b/c.jpg")) + path = pathlib.Path("/a/b/c.jpg") + inval = emmental.tracklist.selection.PlaylistRow("inval", path) + self.assertIsNone(inval._icon.props.custom_image) class TestUserTracksFilter(tests.util.TestCase): @@ -134,35 +116,38 @@ class TestPlaylistView(tests.util.TestCase): def test_init(self): """Test that the Playlist View is set up properly.""" - self.assertIsInstance(self.view, Gtk.ListView) + self.assertIsInstance(self.view, Gtk.ListBox) + self.assertEqual(self.view.props.selection_mode, + Gtk.SelectionMode.NONE) + self.assertTrue(self.view.has_css_class("boxed-list")) - self.assertTrue(self.view.get_show_separators()) - self.assertTrue(self.view.get_single_click_activate()) - self.assertTrue(self.view.has_css_class("rich-list")) - - def test_models(self): - """Test that the models have been connected correctly.""" - self.assertIsInstance(self.view._selection, Gtk.NoSelection) + def test_filter_model(self): + """Test that the filter model has been connected correctly.""" self.assertIsInstance(self.view._filtered, Gtk.FilterListModel) self.assertIsInstance(self.view._filtered.get_filter(), emmental.tracklist.selection.UserTracksFilter) - - self.assertEqual(self.view.get_model(), self.view._selection) - self.assertEqual(self.view._selection.get_model(), self.view._filtered) self.assertEqual(self.view._filtered.get_model(), self.sql.playlists) - def test_factory(self): - """Test that the factory has been configured correctly.""" - self.assertIsInstance(self.view._factory, emmental.factory.Factory) - self.assertEqual(self.view.get_factory(), self.view._factory) - self.assertEqual(self.view._factory.row_type, - emmental.tracklist.selection.PlaylistRow) + self.view.playlist = self.sql.playlists.collection + self.assertEqual(self.view._filtered.get_filter().playlist, + self.sql.playlists.collection) + + def test_create_func(self): + """Test that the PlaylistView creates PlaylistRows correctly.""" + row = self.view.get_row_at_index(0) + self.assertIsInstance(row, emmental.tracklist.selection.PlaylistRow) + self.assertEqual(row.name, "Favorite Tracks") + self.assertEqual(row.image, None) + + self.sql.playlists.favorites.image = tests.util.COVER_JPG + self.assertEqual(row.image, tests.util.COVER_JPG) def test_activate(self): """Test activating a Playlist Row for adding tracks.""" selected = unittest.mock.Mock() self.view.connect("playlist-selected", selected) - self.view.emit("activate", 0) + + self.view.emit("row-activated", self.view.get_row_at_index(0)) selected.assert_called_with(self.view, self.sql.playlists.favorites)