tracklist: Add a Remove Tracks button to the TrackView

The button is placed inside a Gtk.Overlay, and is hidden by default. The
button will be shown when tracks are selected if the current Playlist
has its "user-tracks" property set to True.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2023-01-14 15:17:51 -05:00
parent 911aeb84a1
commit ff9724a274
6 changed files with 121 additions and 11 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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."""

View File

@ -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)

View File

@ -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):

View File

@ -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)