tracklist: Add a LoopButton
This is an ImageToggle button that has been configured to cycle between 3 states corresponding to no looping, playlist looping, and track looping. I also update the Tracklist to look for changes in the visible Playlist to update the Loop button. Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
2d19d78eb6
commit
ed1d990e74
|
@ -4,9 +4,10 @@ from gi.repository import GObject
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
from gi.repository import Gio
|
from gi.repository import Gio
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
|
from ..playlist.playlist import Playlist
|
||||||
|
from ..playlist.previous import Previous
|
||||||
from .. import db
|
from .. import db
|
||||||
from .. import entry
|
from .. import entry
|
||||||
from .. import playlist
|
|
||||||
from . import buttons
|
from . import buttons
|
||||||
from . import trackview
|
from . import trackview
|
||||||
|
|
||||||
|
@ -15,13 +16,13 @@ class Card(Gtk.Box):
|
||||||
"""Our Tracklist."""
|
"""Our Tracklist."""
|
||||||
|
|
||||||
sql = GObject.Property(type=db.Connection)
|
sql = GObject.Property(type=db.Connection)
|
||||||
playlist = GObject.Property(type=playlist.playlist.Playlist)
|
|
||||||
|
|
||||||
def __init__(self, sql: db.Connection, **kwargs):
|
def __init__(self, sql: db.Connection, **kwargs):
|
||||||
"""Set up the Tracklist widget."""
|
"""Set up the Tracklist widget."""
|
||||||
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL,
|
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL,
|
||||||
spacing=6, **kwargs)
|
spacing=6, **kwargs)
|
||||||
self._top_left = Gtk.Box()
|
self._top_left = Gtk.Box()
|
||||||
|
self._top_right = Gtk.Box(sensitive=False)
|
||||||
self._top_box = Gtk.CenterBox(margin_top=6, margin_start=6,
|
self._top_box = Gtk.CenterBox(margin_top=6, margin_start=6,
|
||||||
margin_end=6)
|
margin_end=6)
|
||||||
self._filter = entry.Filter("tracks", hexpand=True,
|
self._filter = entry.Filter("tracks", hexpand=True,
|
||||||
|
@ -31,28 +32,48 @@ class Card(Gtk.Box):
|
||||||
self._visible_cols = buttons.VisibleColumns(self._trackview.columns)
|
self._visible_cols = buttons.VisibleColumns(self._trackview.columns)
|
||||||
self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic",
|
self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic",
|
||||||
has_frame=False, sensitive=False)
|
has_frame=False, sensitive=False)
|
||||||
|
self._loop = buttons.LoopButton()
|
||||||
|
|
||||||
self._top_left.append(self._visible_cols)
|
self._top_left.append(self._visible_cols)
|
||||||
self._top_left.append(self._unselect)
|
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_start_widget(self._top_left)
|
||||||
self._top_box.set_center_widget(self._filter)
|
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._top_box)
|
||||||
self.append(self._trackview)
|
self.append(self._trackview)
|
||||||
|
|
||||||
self._trackview.bind_property("have-selected", self._unselect,
|
self._trackview.bind_property("have-selected", self._unselect,
|
||||||
"sensitive")
|
"sensitive")
|
||||||
self.bind_property("playlist", self._trackview, "playlist")
|
|
||||||
|
|
||||||
self._filter.connect("search-changed", self.__search_changed)
|
self._filter.connect("search-changed", self.__search_changed)
|
||||||
self._unselect.connect("clicked", self.__clear_selection)
|
self._unselect.connect("clicked", self.__clear_selection)
|
||||||
|
self._loop.connect("notify::state", self.__update_loop_state)
|
||||||
|
|
||||||
self.add_css_class("card")
|
self.add_css_class("card")
|
||||||
|
|
||||||
def __clear_selection(self, unselect: Gtk.Button) -> None:
|
def __clear_selection(self, unselect: Gtk.Button) -> None:
|
||||||
self._trackview.clear_selected_tracks()
|
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:
|
def __search_changed(self, filter: entry.Filter) -> None:
|
||||||
self.sql.tracks.filter(filter.get_query())
|
self.sql.tracks.filter(filter.get_query())
|
||||||
|
|
||||||
|
@ -69,3 +90,19 @@ class Card(Gtk.Box):
|
||||||
def columns(self) -> Gio.ListModel:
|
def columns(self) -> Gio.ListModel:
|
||||||
"""Get the columns displayed in the Tracklist."""
|
"""Get the columns displayed in the Tracklist."""
|
||||||
return self._trackview.columns
|
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)
|
||||||
|
|
|
@ -42,3 +42,45 @@ class VisibleColumns(buttons.PopoverButton):
|
||||||
column = Gtk.ColumnViewColumn(factory=factory, title=title,
|
column = Gtk.ColumnViewColumn(factory=factory, title=title,
|
||||||
fixed_width=width)
|
fixed_width=width)
|
||||||
self.popover_child.append_column(column)
|
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
|
||||||
|
|
|
@ -69,3 +69,65 @@ class TestVisibleColumns(unittest.TestCase):
|
||||||
self.assertFalse(switch.child.get_active())
|
self.assertFalse(switch.child.get_active())
|
||||||
switch.child.set_active(True)
|
switch.child.set_active(True)
|
||||||
item.set_visible(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")
|
||||||
|
|
|
@ -13,6 +13,7 @@ class TestTracklist(tests.util.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up common variables."""
|
"""Set up common variables."""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.sql.playlists.load(now=True)
|
||||||
self.tracklist = emmental.tracklist.Card(self.sql)
|
self.tracklist = emmental.tracklist.Card(self.sql)
|
||||||
self.db_plist = self.sql.playlists.create("Test Playlist")
|
self.db_plist = self.sql.playlists.create("Test Playlist")
|
||||||
self.playlist = emmental.playlist.playlist.Playlist(self.sql,
|
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, Gtk.Box)
|
||||||
self.assertIsInstance(self.tracklist._top_box, Gtk.CenterBox)
|
self.assertIsInstance(self.tracklist._top_box, Gtk.CenterBox)
|
||||||
self.assertIsInstance(self.tracklist._top_left, Gtk.Box)
|
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.sql, self.sql)
|
||||||
self.assertEqual(self.tracklist.get_spacing(), 6)
|
self.assertEqual(self.tracklist.get_spacing(), 6)
|
||||||
|
@ -39,6 +41,8 @@ class TestTracklist(tests.util.TestCase):
|
||||||
self.tracklist._top_box)
|
self.tracklist._top_box)
|
||||||
self.assertEqual(self.tracklist._top_box.get_start_widget(),
|
self.assertEqual(self.tracklist._top_box.get_start_widget(),
|
||||||
self.tracklist._top_left)
|
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"))
|
self.assertTrue(self.tracklist.has_css_class("card"))
|
||||||
|
|
||||||
|
@ -89,6 +93,29 @@ class TestTracklist(tests.util.TestCase):
|
||||||
self.tracklist._filter.emit("search-changed")
|
self.tracklist._filter.emit("search-changed")
|
||||||
mock_filter.assert_called_with("*test text*")
|
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):
|
def test_trackview(self):
|
||||||
"""Test the Trackview widget."""
|
"""Test the Trackview widget."""
|
||||||
self.assertIsInstance(self.tracklist._trackview,
|
self.assertIsInstance(self.tracklist._trackview,
|
||||||
|
@ -104,10 +131,20 @@ class TestTracklist(tests.util.TestCase):
|
||||||
def test_playlist(self):
|
def test_playlist(self):
|
||||||
"""Test the playlist property."""
|
"""Test the playlist property."""
|
||||||
self.assertIsNone(self.tracklist.playlist)
|
self.assertIsNone(self.tracklist.playlist)
|
||||||
|
self.assertFalse(self.tracklist._top_right.get_sensitive())
|
||||||
|
|
||||||
self.tracklist.playlist = self.playlist
|
self.tracklist.playlist = self.playlist
|
||||||
self.assertEqual(self.tracklist.playlist, self.playlist)
|
self.assertEqual(self.tracklist.playlist, self.playlist)
|
||||||
self.assertEqual(self.tracklist._trackview.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")
|
@unittest.mock.patch("gi.repository.GLib.idle_add")
|
||||||
def test_scroll_to_track(self, mock_idle_add: unittest.mock.Mock):
|
def test_scroll_to_track(self, mock_idle_add: unittest.mock.Mock):
|
||||||
|
|
Loading…
Reference in New Issue