nowplaying: Add Control buttons
Complete with signals so we'll know when the user wants us to do something. I also clear the autopause property when the user manually pauses the player. I use large versions of the play and pause icons from the Gnome Icon Library for the buttons. Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
bf8d7fac1b
commit
3157c53423
93
emmental/nowplaying/controls.py
Normal file
93
emmental/nowplaying/controls.py
Normal file
|
@ -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."""
|
2
icons/scalable/actions/pause-large-symbolic.svg
Normal file
2
icons/scalable/actions/pause-large-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 2 1 h 5 v 14 h -5 z m 0 0"/><path d="m 9 1 h 5 v 14 h -5 z m 0 0"/></g></svg>
|
After Width: | Height: | Size: 233 B |
2
icons/scalable/actions/play-large-symbolic.svg
Normal file
2
icons/scalable/actions/play-large-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 2 1 v 13.992188 h 1.398438 c 0.246093 0.003906 0.488281 -0.050782 0.699218 -0.171876 l 9.796875 -5.597656 c 0.433594 -0.242187 0.65625 -0.734375 0.65625 -1.226562 c 0 -0.492188 -0.222656 -0.984375 -0.65625 -1.222656 l -9.796875 -5.597657 c -0.210937 -0.121093 -0.453125 -0.175781 -0.699218 -0.175781 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 469 B |
143
tests/nowplaying/test_controls.py
Normal file
143
tests/nowplaying/test_controls.py
Normal file
|
@ -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())
|
Loading…
Reference in New Issue
Block a user