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:
Anna Schumaker 2022-06-30 17:22:18 -04:00
parent bf8d7fac1b
commit 3157c53423
4 changed files with 240 additions and 0 deletions

View 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."""

View 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

View 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

View 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())