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:
parent
911aeb84a1
commit
ff9724a274
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue