From 000dbd701827a362adc92508624dd33df349c2c0 Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Fri, 15 Jul 2022 09:59:04 -0400 Subject: [PATCH] nowplaying: Add a Seeker to the Now Playing card And wire it up to the Player through the Application. Implements: #45 (Create a new NowPlaying widget) Signed-off-by: Anna Schumaker --- emmental/__init__.py | 9 ++++++- emmental/mpris2/player.py | 6 +++++ emmental/nowplaying/__init__.py | 19 +++++++++++++++ tests/nowplaying/test_nowplaying.py | 38 +++++++++++++++++++++++++++++ tests/test_emmental.py | 3 ++- 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/emmental/__init__.py b/emmental/__init__.py index a6c0c78..d079e38 100644 --- a/emmental/__init__.py +++ b/emmental/__init__.py @@ -44,6 +44,11 @@ class Application(Adw.Application): def __load_path(self, src: GObject.GObject, path: pathlib.Path) -> None: self.__load_file(path) + def __on_seek(self, nowplay: nowplaying.Card, newpos: float) -> None: + """Handle a seek event.""" + self.player.seek(newpos) + self.mpris.player.seeked(newpos) + def __seek(self, player: mpris2.player.Player, offset: float) -> None: self.player.seek(self.player.position + offset) @@ -78,13 +83,15 @@ class Application(Adw.Application): playing.bind_property("autopause", self, "autopause", GObject.BindingFlags.BIDIRECTIONAL) for prop in ["title", "album", "artist", "album-artist", - "playing", "have-track"]: + "playing", "position", "duration", "have-track"]: self.player.bind_property(prop, playing, prop) self.db.settings.bind_setting("now-playing.prefer-artist", playing, "prefer-artist") playing.connect("play", self.player.play) playing.connect("pause", self.player.pause) + playing.connect("seek", self.__on_seek) + return playing def build_window(self) -> window.Window: diff --git a/emmental/mpris2/player.py b/emmental/mpris2/player.py index 9d287cf..abe5308 100644 --- a/emmental/mpris2/player.py +++ b/emmental/mpris2/player.py @@ -57,6 +57,12 @@ class Player(dbus.Object): changed = GLib.Variant("b", self.get_property(property)) self.properties_changed({property: changed}) + def seeked(self, newpos: float) -> None: + """Notify that the track position has changed.""" + args = GLib.Variant.new_tuple(GLib.Variant("x", newpos)) + self.dbus.emit_signal(None, dbus.OBJECT_PATH, self.interface.name, + "Seeked", args) + @GObject.Property def Metadata(self) -> dict: """Metadata for the current Track.""" diff --git a/emmental/nowplaying/__init__.py b/emmental/nowplaying/__init__.py index 49c1067..910baaa 100644 --- a/emmental/nowplaying/__init__.py +++ b/emmental/nowplaying/__init__.py @@ -3,6 +3,7 @@ from gi.repository import GObject from gi.repository import Gtk from . import controls +from . import seeker from . import tags @@ -13,6 +14,8 @@ class Card(Gtk.Box): album = GObject.Property(type=str) artist = GObject.Property(type=str) album_artist = GObject.Property(type=str) + duration = GObject.Property(type=float, default=1) + position = GObject.Property(type=float, default=0) prefer_artist = GObject.Property(type=bool, default=True) playing = GObject.Property(type=bool, default=False) autopause = GObject.Property(type=int, default=-1, minimum=-1, maximum=99) @@ -27,6 +30,8 @@ class Card(Gtk.Box): self._grid = Gtk.Grid() self._tags = tags.TagInfo() self._controls = controls.Controls() + self._bottom_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) + self._seeker = seeker.Scale(sensitive=False) for prop in ["title", "album", "artist", "album-artist"]: self.bind_property(prop, self._tags, prop) @@ -34,14 +39,20 @@ class Card(Gtk.Box): GObject.BindingFlags.BIDIRECTIONAL) for prop in ["playing", "have-next", "have-previous", "have-track"]: self.bind_property(prop, self._controls, prop) + self.bind_property("have-track", self._seeker, "sensitive") self.bind_property("autopause", self._controls, "autopause", GObject.BindingFlags.BIDIRECTIONAL) + self.bind_property("duration", self._seeker, "duration") + self.bind_property("position", self._seeker, "position") for sig in ["play", "pause", "previous", "next"]: self._controls.connect(sig, self.__on_control, sig) + self._seeker.connect("change-value", self.__on_seek) + self._bottom_box.append(self._seeker) self._grid.attach(self._tags, 0, 0, 1, 1) self._grid.attach(self._controls, 1, 0, 1, 1) + self._grid.attach(self._bottom_box, 0, 1, 2, 1) self.append(self._grid) self.add_css_class("card") @@ -49,6 +60,10 @@ class Card(Gtk.Box): def __on_control(self, controls: controls.Controls, signal: str) -> None: self.emit(signal) + def __on_seek(self, seek: seeker.Scale, scroll: Gtk.ScrollType, + value: float) -> None: + self.emit("seek", value) + @GObject.Signal def play(self) -> None: """Signal that the Play button has been clicked.""" @@ -64,3 +79,7 @@ class Card(Gtk.Box): @GObject.Signal def next(self) -> None: """Signal that the Nause button has been clicked.""" + + @GObject.Signal(arg_types=(float,)) + def seek(self, newpos: float) -> None: + """Signal that the user wants us to seek.""" diff --git a/tests/nowplaying/test_nowplaying.py b/tests/nowplaying/test_nowplaying.py index c8a5f66..89ed0f4 100644 --- a/tests/nowplaying/test_nowplaying.py +++ b/tests/nowplaying/test_nowplaying.py @@ -17,8 +17,18 @@ class TestNowPlaying(unittest.TestCase): """Test that the card has been initialized correctly.""" self.assertIsInstance(self.card, Gtk.Box) self.assertIsInstance(self.card._grid, Gtk.Grid) + self.assertIsInstance(self.card._bottom_box, Gtk.Box) + + self.assertEqual(self.card._bottom_box.get_orientation(), + Gtk.Orientation.HORIZONTAL) + self.assertEqual(self.card._bottom_box.get_spacing(), 0) self.assertEqual(self.card.get_last_child(), self.card._grid) + self.assertEqual(self.card._grid.get_child_at(0, 1), + self.card._bottom_box) + self.assertEqual(self.card._grid.get_child_at(1, 1), + self.card._bottom_box) + self.assertTrue(self.card.has_css_class("card")) def test_prefer_artist(self): @@ -60,6 +70,22 @@ class TestNowPlaying(unittest.TestCase): self.card.emit(signal) handler.assert_called_with(self.card) + def test_seeker(self): + """Test the seeker widget.""" + self.assertIsInstance(self.card._seeker, + emmental.nowplaying.seeker.Scale) + self.assertEqual(self.card._bottom_box.get_last_child(), + self.card._seeker) + + self.assertFalse(self.card._seeker.get_sensitive()) + self.card.have_track = True + self.assertTrue(self.card._seeker.get_sensitive()) + + handler = unittest.mock.Mock() + self.card.connect("seek", handler) + self.card._seeker.emit("change-value", Gtk.ScrollType.JUMP, 10) + handler.assert_called_with(self.card, 10) + def test_playing(self): """Test the 'playing' property.""" self.assertFalse(self.card.playing) @@ -85,3 +111,15 @@ class TestNowPlaying(unittest.TestCase): self.card.autopause = 3 self.assertEqual(self.card.autopause, 3) self.assertEqual(self.card._controls.autopause, 3) + + def test_duration(self): + """Test the 'duration' property.""" + self.assertEqual(self.card.duration, 1) + self.card.duration = 10 + self.assertEqual(self.card._seeker.duration, 10) + + def test_position(self): + """Test the 'position' property.""" + self.assertEqual(self.card.position, 0) + self.card.position = 0.5 + self.assertEqual(self.card._seeker.position, 0.5) diff --git a/tests/test_emmental.py b/tests/test_emmental.py index f47dcba..c05c154 100644 --- a/tests/test_emmental.py +++ b/tests/test_emmental.py @@ -114,7 +114,8 @@ class TestEmmental(unittest.TestCase): self.application.player = emmental.audio.Player() win = self.application.build_window() - for (property, value) in [("have-track", True), ("playing", True)]: + for (property, value) in [("have-track", True), ("playing", True), + ("duration", 10), ("position", 5)]: with self.subTest(property=property, value=value): self.application.player.set_property(property, value) self.assertEqual(win.now_playing.get_property(property), value)