diff --git a/emmental/tracklist/__init__.py b/emmental/tracklist/__init__.py index d04a20f..c544350 100644 --- a/emmental/tracklist/__init__.py +++ b/emmental/tracklist/__init__.py @@ -4,9 +4,10 @@ from gi.repository import GObject from gi.repository import GLib from gi.repository import Gio from gi.repository import Gtk +from ..playlist.playlist import Playlist +from ..playlist.previous import Previous from .. import db from .. import entry -from .. import playlist from . import buttons from . import trackview @@ -15,13 +16,13 @@ class Card(Gtk.Box): """Our Tracklist.""" sql = GObject.Property(type=db.Connection) - playlist = GObject.Property(type=playlist.playlist.Playlist) def __init__(self, sql: db.Connection, **kwargs): """Set up the Tracklist widget.""" super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL, spacing=6, **kwargs) self._top_left = Gtk.Box() + self._top_right = Gtk.Box(sensitive=False) self._top_box = Gtk.CenterBox(margin_top=6, margin_start=6, margin_end=6) self._filter = entry.Filter("tracks", hexpand=True, @@ -31,28 +32,48 @@ class Card(Gtk.Box): self._visible_cols = buttons.VisibleColumns(self._trackview.columns) self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic", has_frame=False, sensitive=False) + self._loop = buttons.LoopButton() self._top_left.append(self._visible_cols) self._top_left.append(self._unselect) + self._top_right.append(self._loop) + self._top_box.set_start_widget(self._top_left) self._top_box.set_center_widget(self._filter) + self._top_box.set_end_widget(self._top_right) self.append(self._top_box) self.append(self._trackview) self._trackview.bind_property("have-selected", self._unselect, "sensitive") - self.bind_property("playlist", self._trackview, "playlist") self._filter.connect("search-changed", self.__search_changed) self._unselect.connect("clicked", self.__clear_selection) + self._loop.connect("notify::state", self.__update_loop_state) self.add_css_class("card") def __clear_selection(self, unselect: Gtk.Button) -> None: self._trackview.clear_selected_tracks() + def __playlist_notify(self, playlist: Playlist, param) -> None: + match param.name: + case "loop": + self._loop.state = playlist.loop + case "playlist": + self.__set_button_state() + + def __set_button_state(self) -> None: + can_disable = self.playlist.playlist != self.sql.playlists.collection + self._loop.can_disable = can_disable + self._loop.state = self.playlist.loop + + def __update_loop_state(self, loop: buttons.LoopButton, param) -> None: + if self.playlist.loop != loop.state: + self.playlist.loop = loop.state + def __search_changed(self, filter: entry.Filter) -> None: self.sql.tracks.filter(filter.get_query()) @@ -69,3 +90,19 @@ class Card(Gtk.Box): def columns(self) -> Gio.ListModel: """Get the columns displayed in the Tracklist.""" return self._trackview.columns + + @GObject.Property(type=Playlist) + def playlist(self) -> Playlist: + """Get the currently visible Playlist.""" + return self._trackview.playlist + + @playlist.setter + def playlist(self, newval: Playlist) -> None: + if self._trackview.playlist is not None: + self._trackview.playlist.disconnect_by_func(self.__playlist_notify) + + self._trackview.playlist = newval + if newval is not None: + self._top_right.set_sensitive(not isinstance(newval, Previous)) + self.__set_button_state() + newval.connect("notify", self.__playlist_notify) diff --git a/emmental/tracklist/buttons.py b/emmental/tracklist/buttons.py index 1f44d0a..f988443 100644 --- a/emmental/tracklist/buttons.py +++ b/emmental/tracklist/buttons.py @@ -42,3 +42,45 @@ class VisibleColumns(buttons.PopoverButton): column = Gtk.ColumnViewColumn(factory=factory, title=title, fixed_width=width) self.popover_child.append_column(column) + + +class LoopButton(buttons.ImageToggle): + """A button for setting Loop state of a Playlist.""" + + can_disable = GObject.Property(type=bool, default=True) + + def __init__(self, **kwargs): + """Initialize a Loop Button.""" + super().__init__(active_icon_name="media-playlist-repeat-song", + inactive_icon_name="media-playlist-repeat", + icon_size=Gtk.IconSize.NORMAL, state="None", + has_frame=False, **kwargs) + + def do_clicked(self): + """Cycle though Loop states.""" + match (self.state, self.can_disable): + case ("None", _): self.state = "Playlist" + case ("Playlist", _): self.state = "Track" + case ("Track", True): self.state = "None" + case ("Track", False): self.state = "Playlist" + + @GObject.Property(type=str) + def state(self) -> str: + """Get the current state of the button.""" + match (self.active, self.icon_opacity): + case (True, 1.0): return "Track" + case (False, 1.0): return "Playlist" + case (_, _): return "None" + + @state.setter + def state(self, newval: str) -> None: + match (newval, self.can_disable): + case ("None", True): + self.active = False + self.icon_opacity = 0.5 + case ("Playlist", _): + self.active = False + self.icon_opacity = 1.0 + case ("Track", _): + self.active = True + self.icon_opacity = 1.0 diff --git a/tests/tracklist/test_buttons.py b/tests/tracklist/test_buttons.py index 67e0438..6261a9d 100644 --- a/tests/tracklist/test_buttons.py +++ b/tests/tracklist/test_buttons.py @@ -69,3 +69,65 @@ class TestVisibleColumns(unittest.TestCase): self.assertFalse(switch.child.get_active()) switch.child.set_active(True) item.set_visible(True) + + +class TestLoopButton(unittest.TestCase): + """Test the LoopButton.""" + + def setUp(self): + """Set up common variables.""" + self.loop = emmental.tracklist.buttons.LoopButton() + + def test_init(self): + """Test that the loop button is set up properly.""" + self.assertIsInstance(self.loop, emmental.buttons.ImageToggle) + self.assertEqual(self.loop.active_icon_name, + "media-playlist-repeat-song") + self.assertEqual(self.loop.inactive_icon_name, "media-playlist-repeat") + self.assertEqual(self.loop.icon_size, Gtk.IconSize.NORMAL) + self.assertFalse(self.loop.get_has_frame()) + + def test_state(self): + """Test changing the state property.""" + self.assertTrue(self.loop.can_disable) + + self.assertEqual(self.loop.state, "None") + self.assertAlmostEqual(self.loop.icon_opacity, 0.5, delta=0.005) + self.assertFalse(self.loop.active) + + self.loop.state = "Playlist" + self.assertEqual(self.loop.state, "Playlist") + self.assertEqual(self.loop.icon_opacity, 1.0) + self.assertFalse(self.loop.active) + + self.loop.state = "Track" + self.assertEqual(self.loop.state, "Track") + self.assertEqual(self.loop.icon_opacity, 1.0) + self.assertTrue(self.loop.active) + + self.loop.can_disable = False + self.loop.state = "None" + self.assertEqual(self.loop.state, "Track") + self.assertTrue(self.loop.active) + + self.loop.can_disable = True + self.loop.state = "None" + self.assertAlmostEqual(self.loop.icon_opacity, 0.5, delta=0.005) + self.assertFalse(self.loop.active) + + def test_click(self): + """Test cycling through states when clicked.""" + self.assertEqual(self.loop.state, "None") + self.loop.emit("clicked") + self.assertEqual(self.loop.state, "Playlist") + self.loop.emit("clicked") + self.assertEqual(self.loop.state, "Track") + self.loop.emit("clicked") + self.assertEqual(self.loop.state, "None") + + self.loop.state = "Playlist" + self.loop.can_disable = False + self.loop.emit("clicked") + self.assertEqual(self.loop.state, "Track") + self.loop.emit("clicked") + self.assertEqual(self.loop.state, "Playlist") diff --git a/tests/tracklist/test_tracklist.py b/tests/tracklist/test_tracklist.py index 5a15298..b26cea9 100644 --- a/tests/tracklist/test_tracklist.py +++ b/tests/tracklist/test_tracklist.py @@ -13,6 +13,7 @@ class TestTracklist(tests.util.TestCase): def setUp(self): """Set up common variables.""" super().setUp() + self.sql.playlists.load(now=True) self.tracklist = emmental.tracklist.Card(self.sql) self.db_plist = self.sql.playlists.create("Test Playlist") self.playlist = emmental.playlist.playlist.Playlist(self.sql, @@ -23,6 +24,7 @@ class TestTracklist(tests.util.TestCase): self.assertIsInstance(self.tracklist, Gtk.Box) self.assertIsInstance(self.tracklist._top_box, Gtk.CenterBox) self.assertIsInstance(self.tracklist._top_left, Gtk.Box) + self.assertIsInstance(self.tracklist._top_right, Gtk.Box) self.assertEqual(self.tracklist.sql, self.sql) self.assertEqual(self.tracklist.get_spacing(), 6) @@ -39,6 +41,8 @@ class TestTracklist(tests.util.TestCase): self.tracklist._top_box) self.assertEqual(self.tracklist._top_box.get_start_widget(), self.tracklist._top_left) + self.assertEqual(self.tracklist._top_box.get_end_widget(), + self.tracklist._top_right) self.assertTrue(self.tracklist.has_css_class("card")) @@ -89,6 +93,29 @@ class TestTracklist(tests.util.TestCase): self.tracklist._filter.emit("search-changed") mock_filter.assert_called_with("*test text*") + def test_loop_button(self): + """Test the loop button.""" + self.assertIsInstance(self.tracklist._loop, + emmental.tracklist.buttons.LoopButton) + self.assertEqual(self.tracklist._top_right.get_first_child(), + self.tracklist._loop) + + self.tracklist._loop.can_disable = False + self.playlist.loop = "Track" + self.tracklist.playlist = self.playlist + self.assertTrue(self.tracklist._loop.can_disable) + self.assertEqual(self.tracklist._loop.state, "Track") + + self.playlist.playlist = self.sql.playlists.collection + self.assertFalse(self.tracklist._loop.can_disable) + self.assertEqual(self.tracklist._loop.state, "Playlist") + + self.playlist.loop = "Track" + self.assertEqual(self.tracklist._loop.state, "Track") + + self.tracklist._loop.state = "Playlist" + self.assertEqual(self.playlist.loop, "Playlist") + def test_trackview(self): """Test the Trackview widget.""" self.assertIsInstance(self.tracklist._trackview, @@ -104,10 +131,20 @@ class TestTracklist(tests.util.TestCase): def test_playlist(self): """Test the playlist property.""" self.assertIsNone(self.tracklist.playlist) + self.assertFalse(self.tracklist._top_right.get_sensitive()) self.tracklist.playlist = self.playlist self.assertEqual(self.tracklist.playlist, self.playlist) self.assertEqual(self.tracklist._trackview.playlist, self.playlist) + self.assertTrue(self.tracklist._top_right.get_sensitive()) + + db_prev = self.sql.playlists.previous + previous = emmental.playlist.previous.Previous(self.sql, db_prev) + self.tracklist.playlist = previous + self.assertFalse(self.tracklist._top_right.get_sensitive()) + + self.tracklist.playlist.playlist = None + self.tracklist.playlist = None @unittest.mock.patch("gi.repository.GLib.idle_add") def test_scroll_to_track(self, mock_idle_add: unittest.mock.Mock):