diff --git a/emmental/nowplaying/controls.py b/emmental/nowplaying/controls.py new file mode 100644 index 0000000..36ec4ef --- /dev/null +++ b/emmental/nowplaying/controls.py @@ -0,0 +1,93 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Our playback control widgets.""" +from gi.repository import GObject +from gi.repository import Gtk +from . import autopause +from .. import buttons +from .. import window + +MARGIN = 24 + + +class PillButton(buttons.Button): + """A Button with the pill style class.""" + + def __init__(self, **kwargs): + """Initialize a Pill Button.""" + super().__init__(icon_size=Gtk.IconSize.LARGE, **kwargs) + self.add_css_class("pill") + + +class Controls(Gtk.Box): + """Our playback control widgets.""" + + autopause = GObject.Property(type=int, default=-1, minimum=-1, maximum=99) + playing = GObject.Property(type=bool, default=False) + + have_next = GObject.Property(type=bool, default=False) + have_previous = GObject.Property(type=bool, default=False) + have_track = GObject.Property(type=bool, default=False) + + def __init__(self): + """Initialize the Controls.""" + super().__init__(valign=Gtk.Align.START, homogeneous=True, + halign=Gtk.Align.END, hexpand=False, + margin_start=MARGIN/2, margin_end=MARGIN, + margin_top=MARGIN, margin_bottom=MARGIN) + + self._autopause = autopause.Button() + self._prev = PillButton(icon_name="media-skip-backward", + sensitive=False) + self._play = PillButton(icon_name="play-large", sensitive=False) + self._pause = buttons.SplitButton(icon_name="pause-large", + icon_size=Gtk.IconSize.LARGE, + secondary=self._autopause, + visible=False, sensitive=False) + self._next = PillButton(icon_name="media-skip-forward", + sensitive=False) + + for button in [self._prev, self._play, self._pause, self._next]: + self.append(button) + + self._prev.connect("clicked", self.__on_click, "previous") + self._play.connect("clicked", self.__on_click, "play") + self._pause.connect("clicked", self.__on_click, "pause") + self._next.connect("clicked", self.__on_click, "next") + + self.bind_property("autopause", self._autopause, "value", + GObject.BindingFlags.BIDIRECTIONAL) + self.bind_property("playing", self._pause, "visible") + self.bind_property("playing", self._play, "visible", + GObject.BindingFlags.INVERT_BOOLEAN) + self.bind_property("have-next", self._next, "sensitive") + self.bind_property("have-previous", self._prev, "sensitive") + self.bind_property("have-track", self._play, "sensitive") + self.bind_property("have-track", self._pause, "sensitive") + self.connect("notify::playing", self.__notify_playing) + + self.add_css_class("linked") + + def __on_click(self, button: Gtk.Button, signal: str) -> None: + self.emit(signal) + + def __notify_playing(self, controls, param) -> None: + if not self.playing and self.autopause != -1: + if win := self.get_ancestor(window.Window): + win.post_toast("Autopause Cancelled") + self.autopause = -1 + + @GObject.Signal + def previous(self) -> None: + """Signals that the Previous button has been clicked.""" + + @GObject.Signal + def play(self) -> None: + """Signals that the Play button has been clicked.""" + + @GObject.Signal + def pause(self) -> None: + """Signals that the Pause button has been clicked.""" + + @GObject.Signal + def next(self) -> None: + """Signals that the Next button has been clicked.""" diff --git a/icons/scalable/actions/pause-large-symbolic.svg b/icons/scalable/actions/pause-large-symbolic.svg new file mode 100644 index 0000000..f59633d --- /dev/null +++ b/icons/scalable/actions/pause-large-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/icons/scalable/actions/play-large-symbolic.svg b/icons/scalable/actions/play-large-symbolic.svg new file mode 100644 index 0000000..c70e4c5 --- /dev/null +++ b/icons/scalable/actions/play-large-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/tests/nowplaying/test_controls.py b/tests/nowplaying/test_controls.py new file mode 100644 index 0000000..2fd398c --- /dev/null +++ b/tests/nowplaying/test_controls.py @@ -0,0 +1,143 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Tests our playback controls.""" +import unittest +import unittest.mock +import emmental.nowplaying.controls +from gi.repository import Gtk + + +class TestButtons(unittest.TestCase): + """Test the Control Buttons.""" + + def test_pill(self): + """Test that the pill button is configured correctly.""" + button = emmental.nowplaying.controls.PillButton() + self.assertIsInstance(button, emmental.buttons.Button) + self.assertEqual(button.icon_size, Gtk.IconSize.LARGE) + self.assertTrue(button.has_css_class("pill")) + + +class TestControls(unittest.TestCase): + """Tests the Controls widget.""" + + def setUp(self): + """Set up common variables.""" + self.controls = emmental.nowplaying.controls.Controls() + self.clicked = unittest.mock.Mock() + + def test_constants(self): + """Test constant values.""" + self.assertEqual(emmental.nowplaying.controls.MARGIN, 24) + + def test_init(self): + """Test that controls were initialized properly.""" + self.assertIsInstance(self.controls, Gtk.Box) + self.assertEqual(self.controls.get_orientation(), + Gtk.Orientation.HORIZONTAL) + self.assertEqual(self.controls.get_valign(), Gtk.Align.START) + self.assertTrue(self.controls.get_homogeneous()) + self.assertTrue(self.controls.has_css_class("linked")) + + self.assertEqual(self.controls.get_margin_top(), + emmental.nowplaying.controls.MARGIN) + self.assertEqual(self.controls.get_margin_bottom(), + emmental.nowplaying.controls.MARGIN) + self.assertEqual(self.controls.get_margin_start(), + emmental.nowplaying.controls.MARGIN / 2) + self.assertEqual(self.controls.get_margin_end(), + emmental.nowplaying.controls.MARGIN) + + def test_previous_button(self): + """Test the previous button.""" + self.assertIsInstance(self.controls._prev, + emmental.nowplaying.controls.PillButton) + self.assertEqual(self.controls._prev.icon_name, "media-skip-backward") + self.assertEqual(self.controls.get_first_child(), self.controls._prev) + + self.controls._prev.connect("clicked", self.clicked) + self.controls._prev.emit("clicked") + self.clicked.assert_called_with(self.controls._prev) + + def test_play_button(self): + """Test the play button.""" + self.assertIsInstance(self.controls._play, + emmental.nowplaying.controls.PillButton) + self.assertEqual(self.controls._play.icon_name, "play-large") + self.assertEqual(self.controls._prev.get_next_sibling(), + self.controls._play) + + self.assertFalse(self.controls.playing) + self.assertTrue(self.controls._play.get_visible()) + self.controls.playing = True + self.assertFalse(self.controls._play.get_visible()) + self.controls.playing = False + self.assertTrue(self.controls._play.get_visible()) + + self.controls._play.connect("clicked", self.clicked) + self.controls._play.emit("clicked") + self.clicked.assert_called_with(self.controls._play) + + def test_pause_button(self): + """Test the pause button.""" + self.assertIsInstance(self.controls._pause, + emmental.buttons.SplitButton) + self.assertEqual(self.controls._pause.icon_name, "pause-large") + self.assertEqual(self.controls._pause.icon_size, + Gtk.IconSize.LARGE) + self.assertEqual(self.controls._play.get_next_sibling(), + self.controls._pause) + + self.assertFalse(self.controls._pause.get_visible()) + self.controls.playing = True + self.assertTrue(self.controls._pause.get_visible()) + self.controls.playing = False + self.assertFalse(self.controls._pause.get_visible()) + + self.controls._pause.connect("clicked", self.clicked) + self.controls._pause.emit("clicked") + self.clicked.assert_called_with(self.controls._pause) + + def test_autopause_button(self): + """Test the autopause button.""" + self.assertIsInstance(self.controls._autopause, + emmental.nowplaying.autopause.Button) + self.assertEqual(self.controls._pause.secondary, + self.controls._autopause) + self.assertEqual(self.controls.autopause, -1) + + self.controls._autopause.value = 42 + self.assertEqual(self.controls.autopause, 42) + self.controls.autopause = 21 + self.assertEqual(self.controls._autopause.value, 21) + self.controls.playing = False + self.assertEqual(self.controls.autopause, -1) + + def test_next_button(self): + """Test the next button.""" + self.assertIsInstance(self.controls._next, + emmental.nowplaying.controls.PillButton) + self.assertEqual(self.controls._next.icon_name, "media-skip-forward") + + self.controls._next.connect("clicked", self.clicked) + self.controls._next.emit("clicked") + self.clicked.assert_called_with(self.controls._next) + + def test_have_properties(self): + """Test the have_{next, previous, track} properties.""" + self.assertFalse(self.controls.have_next) + self.assertFalse(self.controls.have_previous) + self.assertFalse(self.controls.have_track) + + for button in [self.controls._next, self.controls._prev, + self.controls._play, self.controls._pause]: + self.assertFalse(button.get_sensitive()) + + self.controls.have_next = True + self.assertTrue(self.controls._next.get_sensitive()) + + self.controls.have_previous = True + self.assertTrue(self.controls._prev.get_sensitive()) + + self.controls.have_track = True + self.assertTrue(self.controls._play.get_sensitive()) + self.assertTrue(self.controls._pause.get_sensitive())