header: Add new volume control widgets to the Header class

They live in a Gtk.MenuButton with a custom popover box that can have
replaygain options added to it. I also modify the Application to save
the volume when it is changed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-05-31 10:18:53 -04:00
parent 847c15f64b
commit 081cae4cd8
4 changed files with 76 additions and 1 deletions

View File

@ -30,7 +30,9 @@ class Application(Adw.Application):
def build_header(self) -> header.Header: def build_header(self) -> header.Header:
"""Build a new header instance.""" """Build a new header instance."""
return header.Header(sql=self.db, title=VERSION_STRING) hdr = header.Header(sql=self.db, title=VERSION_STRING)
self.db.settings.bind_setting("audio.volume", hdr, "volume")
return hdr
def build_window(self) -> window.Window: def build_window(self) -> window.Window:
"""Build a new window instance.""" """Build a new window instance."""

View File

@ -4,26 +4,49 @@ from gi.repository import GObject
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Adw from gi.repository import Adw
from .. import db from .. import db
from .. import buttons
from . import volume
if __debug__: if __debug__:
from . import settings from . import settings
SUBTITLE = "The Cheesy Music Player" SUBTITLE = "The Cheesy Music Player"
def _volume_icon(vol: float) -> str:
if vol == 0.0:
return "audio-volume-muted-symbolic"
if vol <= 1/3:
return "audio-volume-low-symbolic"
if vol <= 2/3:
return "audio-volume-medium-symbolic"
return "audio-volume-high-symbolic"
class Header(Gtk.HeaderBar): class Header(Gtk.HeaderBar):
"""Our custom Gtk.HeaderBar containing window title and volume controls.""" """Our custom Gtk.HeaderBar containing window title and volume controls."""
sql = GObject.Property(type=db.Connection) sql = GObject.Property(type=db.Connection)
title = GObject.Property(type=str) title = GObject.Property(type=str)
subtitle = GObject.Property(type=str) subtitle = GObject.Property(type=str)
volume = GObject.Property(type=float, default=1.0)
def __init__(self, sql: db.Connection, title: str): def __init__(self, sql: db.Connection, title: str):
"""Initialize the HeaderBar.""" """Initialize the HeaderBar."""
super().__init__(title=title, subtitle=SUBTITLE, sql=sql) super().__init__(title=title, subtitle=SUBTITLE, sql=sql)
self._title = Adw.WindowTitle(title=self.title, subtitle=self.subtitle) self._title = Adw.WindowTitle(title=self.title, subtitle=self.subtitle)
self._volume = volume.Controls()
self._box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
self._box.append(self._volume)
icon = _volume_icon(self.volume)
self._button = buttons.PopoverButton(popover_child=self._box,
icon_name=icon)
self.bind_property("title", self._title, "title") self.bind_property("title", self._title, "title")
self.bind_property("subtitle", self._title, "subtitle") self.bind_property("subtitle", self._title, "subtitle")
self.bind_property("volume", self._volume, "volume",
GObject.BindingFlags.BIDIRECTIONAL)
if __debug__: if __debug__:
self._window = settings.Window(sql) self._window = settings.Window(sql)
@ -31,8 +54,13 @@ class Header(Gtk.HeaderBar):
self._settings.connect("clicked", self.__run_settings) self._settings.connect("clicked", self.__run_settings)
self.pack_start(self._settings) self.pack_start(self._settings)
self.pack_end(self._button)
self.set_title_widget(self._title) self.set_title_widget(self._title)
self.connect("notify::volume", self.__notify_volume)
def __run_settings(self, button: Gtk.Button) -> None: def __run_settings(self, button: Gtk.Button) -> None:
if __debug__: if __debug__:
self._window.present() self._window.present()
def __notify_volume(self, header, param) -> None:
self._button.set_icon_name(_volume_icon(self.volume))

View File

@ -46,3 +46,40 @@ class TestHeader(tests.util.TestCase):
"present") as mock_present: "present") as mock_present:
self.header._settings.emit("clicked") self.header._settings.emit("clicked")
mock_present.assert_called() mock_present.assert_called()
def test_volume(self):
"""Check that volume widgets work as expected."""
self.assertIsInstance(self.header._volume,
emmental.header.volume.Controls)
self.assertEqual(self.header.volume, 1.0)
for i, vol in [(x, x/10) for x in range(11)]:
with self.subTest(volume=vol):
widget = self.header if i % 2 else self.header._volume
match i:
case 0: icon = "muted"
case 1 | 2 | 3: icon = "low"
case 4 | 5 | 6: icon = "medium"
case _: icon = "high"
widget.volume = vol
self.assertEqual(self.header.volume, vol)
self.assertEqual(self.header._volume.volume, vol)
self.assertEqual(self.header._button.get_icon_name(),
f"audio-volume-{icon}-symbolic")
def test_popover(self):
"""Check that the menu popover was set up correctly."""
self.assertIsInstance(self.header._button,
emmental.buttons.PopoverButton)
self.assertIsInstance(self.header._box, Gtk.Box)
self.assertEqual(self.header._box.get_orientation(),
Gtk.Orientation.VERTICAL)
self.assertEqual(self.header._box.get_spacing(), 0)
self.assertEqual(self.header._button.get_icon_name(),
"audio-volume-high-symbolic")
self.assertEqual(self.header._button.popover_child, self.header._box)
self.assertEqual(self.header._box.get_first_child(),
self.header._volume)

View File

@ -32,3 +32,11 @@ class TestSettings(unittest.TestCase):
win = self.app.build_window() win = self.app.build_window()
self.assertEqual(win.get_default_size(), (100, 200)) self.assertEqual(win.get_default_size(), (100, 200))
def test_save_volume(self):
"""Check saving and loading volume from the database."""
self.assertEqual(self.settings["audio.volume"], 1.0)
self.win.header.volume = 0.5
self.assertEqual(self.settings["audio.volume"], 0.5)
self.assertEqual(self.app.build_header().volume, 0.5)