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