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 <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
9f240bbc8b
commit
5011db344e
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue