diff --git a/emmental/tracklist/selection.py b/emmental/tracklist/selection.py new file mode 100644 index 0000000..d04a1b6 --- /dev/null +++ b/emmental/tracklist/selection.py @@ -0,0 +1,31 @@ +# Copyright 2023 (c) Anna Schumaker. +"""An OSD to show when tracks are selected.""" +from gi.repository import GObject +from gi.repository import Gtk +from .. import playlist + + +class OSD(Gtk.Overlay): + """An Overlay with extra controls for the Tracklist.""" + + playlist = GObject.Property(type=playlist.playlist.Playlist) + selection = GObject.Property(type=Gtk.SelectionModel) + + have_selected = GObject.Property(type=bool, default=False) + n_selected = GObject.Property(type=int) + + def __init__(self, selection: Gtk.SelectionModel, **kwargs): + """Initialize an OSD.""" + super().__init__(selection=selection, **kwargs) + + self.selection.connect("selection-changed", self.__selection_changed) + + 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 + + def clear_selection(self, *args) -> None: + """Clear the current selection.""" + self.selection.unselect_all() + self.__selection_changed(self.selection, 0, 0) diff --git a/emmental/tracklist/trackview.py b/emmental/tracklist/trackview.py index 69e33c3..3973303 100644 --- a/emmental/tracklist/trackview.py +++ b/emmental/tracklist/trackview.py @@ -7,6 +7,7 @@ from .. import db from .. import factory from .. import playlist from . import row +from . import selection class TrackView(Gtk.Frame): @@ -16,6 +17,9 @@ class TrackView(Gtk.Frame): n_tracks = GObject.Property(type=int) runtime = GObject.Property(type=float) + n_selected = GObject.Property(type=int) + have_selected = GObject.Property(type=bool, default=False) + def __init__(self, sql: db.Connection, **kwargs): """Initialize a TrackView.""" super().__init__(**kwargs) @@ -27,6 +31,7 @@ class TrackView(Gtk.Frame): enable_rubberband=True, model=self._selection) self._scrollwin = Gtk.ScrolledWindow(child=self._columnview) + self._osd = selection.OSD(self._selection, child=self._scrollwin) self.__append_column("Art", "cover", row.AlbumCover, resizable=False) self.__append_column("Fav", "favorite", row.FavoriteButton, @@ -52,13 +57,16 @@ class TrackView(Gtk.Frame): self.__append_column("Filepath", "path", row.PathString, visible=False) self.bind_property("playlist", self._filtermodel, "model") + self.bind_property("playlist", self._osd, "playlist") + self._osd.bind_property("have-selected", self, "have-selected") + self._osd.bind_property("n-selected", self, "n-selected") self._selection.bind_property("n-items", self, "n-tracks") self._selection.connect("items-changed", self.__runtime_changed) self._columnview.connect("activate", self.__track_activated) self._columnview.add_css_class("emmental-track-list") - self.set_child(self._scrollwin) + self.set_child(self._osd) def __append_column(self, title: str, property: str, row_type: type, *, width: int = -1, visible: bool = True, @@ -87,6 +95,10 @@ class TrackView(Gtk.Frame): pos = max(i - 3, 0) * adjustment.get_upper() adjustment.set_value(pos / self._selection.get_n_items()) + def clear_selected_tracks(self) -> None: + """Clear the currently selected tracks.""" + self._osd.clear_selection() + @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 new file mode 100644 index 0000000..d60a394 --- /dev/null +++ b/tests/tracklist/test_selection.py @@ -0,0 +1,49 @@ +# Copyright 2023 (c) Anna Schumaker. +"""Tests our Tracklist Selection OSD.""" +import emmental.buttons +import emmental.tracklist.selection +import tests.util +from gi.repository import Gtk + + +class TestOsd(tests.util.TestCase): + """Test the Tracklist OSD.""" + + def setUp(self): + """Set up common variables.""" + super().setUp() + self.db_plist = self.sql.playlists.create("Test Playlist") + self.playlist = emmental.playlist.playlist.Playlist(self.sql, + self.db_plist) + self.model = Gtk.StringList.new(["Test", "OSD", "Strings"]) + self.selection = Gtk.MultiSelection(model=self.model) + self.osd = emmental.tracklist.selection.OSD(self.selection) + + 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_selection_properties(self): + """Test updating properties when the selection changes.""" + self.assertFalse(self.osd.have_selected) + self.assertEqual(self.osd.n_selected, 0) + + self.selection.select_item(1, True) + self.assertTrue(self.osd.have_selected) + self.assertEqual(self.osd.n_selected, 1) + + def test_clear_selection(self): + """Test the clear_selection() function.""" + self.selection.select_item(1, True) + self.osd.clear_selection() + self.assertEqual(self.selection.get_selection().get_size(), 0) + self.assertEqual(self.osd.n_selected, 0) + self.assertFalse(self.osd.have_selected) + + self.osd.playlist = self.playlist + self.assertEqual(self.selection.get_selection().get_size(), 0) + self.assertEqual(self.osd.n_selected, 0) + self.assertFalse(self.osd.have_selected) diff --git a/tests/tracklist/test_trackview.py b/tests/tracklist/test_trackview.py index d1517da..8553b19 100644 --- a/tests/tracklist/test_trackview.py +++ b/tests/tracklist/test_trackview.py @@ -30,8 +30,12 @@ class TestTrackView(tests.util.TestCase): """Test that the TrackView is initialized properly.""" self.assertIsInstance(self.trackview, Gtk.Frame) self.assertIsInstance(self.trackview._scrollwin, Gtk.ScrolledWindow) + self.assertIsInstance(self.trackview._osd, + emmental.tracklist.selection.OSD) - self.assertEqual(self.trackview.get_child(), self.trackview._scrollwin) + self.assertEqual(self.trackview.get_child(), self.trackview._osd) + self.assertEqual(self.trackview._osd.get_child(), + self.trackview._scrollwin) self.assertEqual(self.trackview._scrollwin.get_child(), self.trackview._columnview) @@ -76,12 +80,20 @@ class TestTrackView(tests.util.TestCase): requested.assert_called_with(self.playlist, self.track) mock_unselect.assert_called() + def test_clear_selected_tracks(self): + """Test the clear_selected_tracks() function.""" + with unittest.mock.patch.object(self.trackview._osd, + "clear_selection") as mock_clear: + self.trackview.clear_selected_tracks() + mock_clear.assert_called() + def test_playlist(self): """Test the playlist property.""" self.assertIsNone(self.trackview.playlist) self.trackview.playlist = self.playlist self.assertEqual(self.trackview._filtermodel.get_model(), self.playlist) + self.assertEqual(self.trackview._osd.playlist, self.playlist) def test_n_tracks(self): """Test the n-tracks property.""" @@ -97,6 +109,17 @@ class TestTrackView(tests.util.TestCase): self.db_plist.add_track(self.track) self.assertEqual(self.trackview.runtime, 10.0) + def test_n_selected(self): + """Test the n-selected and have-selected properties.""" + self.assertEqual(self.trackview.n_selected, 0) + self.assertFalse(self.trackview.have_selected) + + self.trackview._osd.n_selected = 4 + self.trackview._osd.have_selected = True + + self.assertEqual(self.trackview.n_selected, 4) + self.assertTrue(self.trackview.have_selected) + class TestTrackViewColumns(tests.util.TestCase): """Test the TrackView Columns."""