diff --git a/emmental/tracklist/__init__.py b/emmental/tracklist/__init__.py index ad89575..ac06e9f 100644 --- a/emmental/tracklist/__init__.py +++ b/emmental/tracklist/__init__.py @@ -89,6 +89,7 @@ class Card(Gtk.Box): self._loop.state = self.playlist.loop self._shuffle.active = self.playlist.shuffle self._sort.set_sort_order(self.playlist.sort_order) + self._trackview.reset_osd() def __update_loop_state(self, loop: buttons.LoopButton, param) -> None: if self.playlist.loop != loop.state: diff --git a/emmental/tracklist/selection.py b/emmental/tracklist/selection.py index 9d7bf4c..24c5cd6 100644 --- a/emmental/tracklist/selection.py +++ b/emmental/tracklist/selection.py @@ -2,6 +2,7 @@ """An OSD to show when tracks are selected.""" from gi.repository import GObject from gi.repository import Gtk +from gi.repository import Adw from .. import playlist @@ -17,14 +18,47 @@ class OSD(Gtk.Overlay): def __init__(self, selection: Gtk.SelectionModel, **kwargs): """Initialize an OSD.""" super().__init__(selection=selection, **kwargs) + self._remove = Gtk.Button(child=Adw.ButtonContent(label="Remove", + icon_name="list-remove-symbolic"), + halign=Gtk.Align.END, valign=Gtk.Align.END, + margin_end=16, margin_bottom=16, + visible=False) + + self._remove.add_css_class("destructive-action") + self._remove.add_css_class("pill") self.selection.connect("selection-changed", self.__selection_changed) + self._remove.connect("clicked", self.__remove_clicked) + + self.add_overlay(self._remove) + + def __get_selected_tracks(self) -> list: + selection = self.selection.get_selection() + return [self.selection.get_item(selection.get_nth(n)) + for n in range(selection.get_size())] + + def __remove_clicked(self, button: Gtk.Button) -> None: + if self.playlist is not None: + for track in self.__get_selected_tracks(): + self.playlist.remove_track(track) + self.clear_selection() def __selection_changed(self, selection: Gtk.SelectionModel, position: int, n_items: int) -> None: self.n_selected = selection.get_selection().get_size() self.have_selected = self.n_selected > 0 + self.__update_visibility() + + def __update_visibility(self) -> None: + db_plist = None if self.playlist is None else self.playlist.playlist + user = False if db_plist is None else db_plist.user_tracks + self._remove.set_visible(user and self.have_selected) def clear_selection(self, *args) -> None: """Clear the current selection.""" self.selection.unselect_all() + + def reset(self) -> None: + """Reset the OSD.""" + self.selection.unselect_all() + self.__selection_changed(self.selection, 0, 0) diff --git a/emmental/tracklist/trackview.py b/emmental/tracklist/trackview.py index 3973303..174e1dc 100644 --- a/emmental/tracklist/trackview.py +++ b/emmental/tracklist/trackview.py @@ -99,6 +99,10 @@ class TrackView(Gtk.Frame): """Clear the currently selected tracks.""" self._osd.clear_selection() + def reset_osd(self) -> None: + """Reset the OSD.""" + self._osd.reset() + @GObject.Property(type=Gio.ListModel) def columns(self) -> Gio.ListModel: """Get the ListModel for the columns.""" diff --git a/tests/tracklist/test_selection.py b/tests/tracklist/test_selection.py index d60a394..17c9642 100644 --- a/tests/tracklist/test_selection.py +++ b/tests/tracklist/test_selection.py @@ -3,7 +3,9 @@ import emmental.buttons import emmental.tracklist.selection import tests.util +import unittest.mock from gi.repository import Gtk +from gi.repository import Adw class TestOsd(tests.util.TestCase): @@ -22,10 +24,57 @@ class TestOsd(tests.util.TestCase): def test_init(self): """Test that the OSD is set up properly.""" self.assertIsInstance(self.osd, Gtk.Overlay) - self.assertEqual(self.osd.selection, self.selection) self.assertIsNone(self.osd.playlist) + def test_remove_button(self): + """Test the remove tracks button.""" + self.assertIsInstance(self.osd._remove, Gtk.Button) + self.assertIsInstance(self.osd._remove.get_child(), Adw.ButtonContent) + self.assertEqual(self.osd._remove.get_child().get_icon_name(), + "list-remove-symbolic") + self.assertEqual(self.osd._remove.get_child().get_label(), "Remove") + self.assertEqual(self.osd._remove.get_halign(), Gtk.Align.END) + self.assertEqual(self.osd._remove.get_valign(), Gtk.Align.END) + self.assertEqual(self.osd._remove.get_margin_end(), 16) + self.assertEqual(self.osd._remove.get_margin_bottom(), 16) + + self.assertTrue(self.osd._remove.has_css_class("destructive-action")) + self.assertTrue(self.osd._remove.has_css_class("pill")) + + self.assertIn(self.osd._remove, self.osd) + + def test_remove_button_visible(self): + """Test the remove button visiblity.""" + self.assertFalse(self.osd._remove.get_visible()) + + self.selection.select_item(0, True) + self.assertFalse(self.osd._remove.get_visible()) + + self.db_plist.user_tracks = False + self.osd.playlist = self.playlist + self.selection.select_item(1, True) + self.assertFalse(self.osd._remove.get_visible()) + + self.db_plist.user_tracks = True + self.selection.select_item(2, True) + self.assertTrue(self.osd._remove.get_visible()) + + def test_remove_button_click(self): + """Test clicking the remove button.""" + with unittest.mock.patch.object(self.db_plist, + "remove_track") as mock_remove: + self.selection.select_all() + self.osd._remove.emit("clicked") + mock_remove.assert_not_called() + + self.osd.playlist = self.playlist + self.selection.select_all() + self.osd._remove.emit("clicked") + mock_remove.assert_has_calls([unittest.mock.call(self.model[0]), + unittest.mock.call(self.model[1]), + unittest.mock.call(self.model[2])]) + def test_selection_properties(self): """Test updating properties when the selection changes.""" self.assertFalse(self.osd.have_selected) @@ -47,3 +96,13 @@ class TestOsd(tests.util.TestCase): self.assertEqual(self.selection.get_selection().get_size(), 0) self.assertEqual(self.osd.n_selected, 0) self.assertFalse(self.osd.have_selected) + + def test_reset(self): + """Test the reset() function.""" + self.osd.have_selected = True + + with unittest.mock.patch.object(self.selection, + "unselect_all") as mock_unselect: + self.osd.reset() + mock_unselect.assert_called() + self.assertFalse(self.osd.have_selected) diff --git a/tests/tracklist/test_tracklist.py b/tests/tracklist/test_tracklist.py index 57215dc..c8d6ded 100644 --- a/tests/tracklist/test_tracklist.py +++ b/tests/tracklist/test_tracklist.py @@ -192,18 +192,23 @@ class TestTracklist(tests.util.TestCase): 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()) + with unittest.mock.patch.object(self.tracklist._trackview, + "reset_osd") as mock_reset_osd: + 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()) + mock_reset_osd.assert_called() - 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()) + mock_reset_osd.reset_mock() + 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()) + mock_reset_osd.assert_called() - self.tracklist.playlist.playlist = None - self.tracklist.playlist = None + 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): diff --git a/tests/tracklist/test_trackview.py b/tests/tracklist/test_trackview.py index 8553b19..2c027b7 100644 --- a/tests/tracklist/test_trackview.py +++ b/tests/tracklist/test_trackview.py @@ -87,6 +87,13 @@ class TestTrackView(tests.util.TestCase): self.trackview.clear_selected_tracks() mock_clear.assert_called() + def test_reset_osd(self): + """Test the reset_osd() function.""" + with unittest.mock.patch.object(self.trackview._osd, + "reset") as mock_reset: + self.trackview.reset_osd() + mock_reset.assert_called() + def test_playlist(self): """Test the playlist property.""" self.assertIsNone(self.trackview.playlist)