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 Gio
|
||||
from gi.repository import Gtk
|
||||
from ..playlist.playlist import Playlist
|
||||
from ..playlist.previous import Previous
|
||||
from .. import db
|
||||
from .. import entry
|
||||
from .. import playlist
|
||||
from . import buttons
|
||||
from . import trackview
|
||||
|
||||
|
@ -15,13 +16,13 @@ class Card(Gtk.Box):
|
|||
"""Our Tracklist."""
|
||||
|
||||
sql = GObject.Property(type=db.Connection)
|
||||
playlist = GObject.Property(type=playlist.playlist.Playlist)
|
||||
|
||||
def __init__(self, sql: db.Connection, **kwargs):
|
||||
"""Set up the Tracklist widget."""
|
||||
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL,
|
||||
spacing=6, **kwargs)
|
||||
self._top_left = Gtk.Box()
|
||||
self._top_right = Gtk.Box(sensitive=False)
|
||||
self._top_box = Gtk.CenterBox(margin_top=6, margin_start=6,
|
||||
margin_end=6)
|
||||
self._filter = entry.Filter("tracks", hexpand=True,
|
||||
|
@ -31,28 +32,48 @@ class Card(Gtk.Box):
|
|||
self._visible_cols = buttons.VisibleColumns(self._trackview.columns)
|
||||
self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic",
|
||||
has_frame=False, sensitive=False)
|
||||
self._loop = buttons.LoopButton()
|
||||
|
||||
self._top_left.append(self._visible_cols)
|
||||
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_center_widget(self._filter)
|
||||
self._top_box.set_end_widget(self._top_right)
|
||||
|
||||
self.append(self._top_box)
|
||||
self.append(self._trackview)
|
||||
|
||||
self._trackview.bind_property("have-selected", self._unselect,
|
||||
"sensitive")
|
||||
self.bind_property("playlist", self._trackview, "playlist")
|
||||
|
||||
self._filter.connect("search-changed", self.__search_changed)
|
||||
self._unselect.connect("clicked", self.__clear_selection)
|
||||
self._loop.connect("notify::state", self.__update_loop_state)
|
||||
|
||||
self.add_css_class("card")
|
||||
|
||||
def __clear_selection(self, unselect: Gtk.Button) -> None:
|
||||
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:
|
||||
self.sql.tracks.filter(filter.get_query())
|
||||
|
||||
|
@ -69,3 +90,19 @@ class Card(Gtk.Box):
|
|||
def columns(self) -> Gio.ListModel:
|
||||
"""Get the columns displayed in the Tracklist."""
|
||||
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,
|
||||
fixed_width=width)
|
||||
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())
|
||||
switch.child.set_active(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):
|
||||
"""Set up common variables."""
|
||||
super().setUp()
|
||||
self.sql.playlists.load(now=True)
|
||||
self.tracklist = emmental.tracklist.Card(self.sql)
|
||||
self.db_plist = self.sql.playlists.create("Test Playlist")
|
||||
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._top_box, Gtk.CenterBox)
|
||||
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.get_spacing(), 6)
|
||||
|
@ -39,6 +41,8 @@ class TestTracklist(tests.util.TestCase):
|
|||
self.tracklist._top_box)
|
||||
self.assertEqual(self.tracklist._top_box.get_start_widget(),
|
||||
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"))
|
||||
|
||||
|
@ -89,6 +93,29 @@ class TestTracklist(tests.util.TestCase):
|
|||
self.tracklist._filter.emit("search-changed")
|
||||
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):
|
||||
"""Test the Trackview widget."""
|
||||
self.assertIsInstance(self.tracklist._trackview,
|
||||
|
@ -104,10 +131,20 @@ class TestTracklist(tests.util.TestCase):
|
|||
def test_playlist(self):
|
||||
"""Test the playlist property."""
|
||||
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())
|
||||
|
||||
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")
|
||||
def test_scroll_to_track(self, mock_idle_add: unittest.mock.Mock):
|
||||
|
|
Loading…
Reference in New Issue