Compare commits

...

11 Commits

Author SHA1 Message Date
Anna Schumaker f307c92edb Emmental 3.0.1
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-01 11:14:40 -04:00
Anna Schumaker 8afd1a6240 audio: Replace the playbin audio-filter with the new filter
And wire up the bg-enabled and bg-volume properties from the header to
the playbin properties with the same name.

Implements: #50 ("Background Music Mode")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-01 11:13:48 -04:00
Anna Schumaker 84fbd94aa1 audio: Create a new audio filter for ReplayGain and Background Mode
This links together our ReplayGain filter with a volume element that is
set to the user configured background volume when background listening
mode is enabled, and 100% when background listening mode is disabled.

Implements: #50 ("Background Music Mode")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-01 11:13:46 -04:00
Anna Schumaker 7155fa9db5 header: Add an icon for background mode to the Header
I also update the button tooltip to display the background listening
status along with the current volume level.

Implements: #50 ("Background Music Mode")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-01 11:13:44 -04:00
Anna Schumaker 0e40e6a4e8 header: Add a BackgroundRow to the popover menu
And wire up the properties so we can save the user's current setting.

Implements: #50 ("Background Music Mode")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-01 11:13:42 -04:00
Anna Schumaker deea9caa37 header: Create a BackgroundRow ListBox row
This widget will be added to the Volume popover and allow configuring a
lower volume for background listening while focusing on tasks.

Implements: #50 ("Background Music Mode")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-01 11:13:39 -04:00
Anna Schumaker a626a1f3c4 header: Convert the volume button box to a Gtk.ListBox
And give it the boxed-list style class so it looks nice.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-05-31 16:31:31 -04:00
Anna Schumaker dae588bfaf header: Convert the ReplayGain selector into an Adw.ExpanderRow
This will be added to a ListBox with the volume controls. Expanding the
row will enable ReplayGain and give the user a menu to select ReplayGain
mode.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-05-31 16:31:31 -04:00
Anna Schumaker 03e5b9ad1b header: Convert the volume controls into a ListBoxRow
The plan is to convert the volume control panel into a Gtk.ListBox for a
more modern appearance.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-05-31 16:31:30 -04:00
Anna Schumaker 5b0a0f54e4 header: Add tooltips to the Header widgets
This is nice to have so users know what to expect from each button. I do
take some liberties, such as putting version numbers of our dependencies
as the tooltip for the Title widget. I also display the current volume
level in the volume button tooltip.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-05-30 13:54:21 -04:00
Anna Schumaker dd9d6268ff gsetup: Rework version string handling
I split the function into two. Calling env_string() will return the
string we generate with the versions of the various modules we depend
on. Calling print_env() will print this string.

I need env_string() so we can set the environment as a tooltip in the
header.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-05-30 13:19:51 -04:00
20 changed files with 658 additions and 201 deletions

View File

@ -8,13 +8,13 @@ other playlists run out of tracks.
* MPRIS2
* ReplayGain
* Gapless playback
* Background listening mode
* Automatically pause after a user-configured number of tracks
* Playlist creation and management
* Automatic playlists based on Artists, Albums, Genres, Decades, and Years
* Multiple library path support
* Plays all audio formats supported by GStreamer
* Renamed track detection (using MusicBrainzIDs)
* Updated tag detection
* Renamed and updated tracks detection (using MusicBrainzIDs)
## Dependencies
* Python3

View File

@ -127,8 +127,11 @@ class Application(Adw.Application):
def build_header(self) -> header.Header:
"""Build a new header instance."""
hdr = header.Header(sql=self.db, title=VERSION_STRING)
hdr.bind_property("volume", self.player, "volume")
for prop in ["bg-enabled", "bg-volume", "volume"]:
hdr.bind_property(prop, self.player, prop)
for (setting, property) in [("audio.volume", "volume"),
("audio.background.enabled", "bg-enabled"),
("audio.background.volume", "bg-volume"),
("audio.replaygain.enabled", "rg-enabled"),
("audio.replaygain.mode", "rg-mode")]:
self.db.settings.bind_setting(setting, hdr, property)
@ -250,7 +253,7 @@ class Application(Adw.Application):
"""Handle any command line options."""
if opts.contains("version"):
print(VERSION_STRING)
gsetup.print_versions()
gsetup.print_env()
return 0
return -1

View File

@ -4,7 +4,7 @@ import pathlib
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gst
from . import replaygain
from . import filter
from .. import path
from .. import tmpdir
@ -35,16 +35,18 @@ class Player(GObject.GObject):
playtime = GObject.Property(type=float)
savedtime = GObject.Property(type=float)
bg_enabled = GObject.Property(type=bool, default=False)
bg_volume = GObject.Property(type=float, default=0.5)
pause_on_load = GObject.Property(type=bool, default=False)
def __init__(self):
"""Initialize the audio Player."""
super().__init__()
self._replaygain = replaygain.Filter()
self._filter = filter.Filter()
self._timeout = None
self._playbin = Gst.ElementFactory.make("playbin")
self._playbin.set_property("audio-filter", self._replaygain)
self._playbin.set_property("audio-filter", self._filter)
self._playbin.set_property("video-sink",
Gst.ElementFactory.make("fakesink"))
self._playbin.set_state(Gst.State.READY)
@ -58,6 +60,8 @@ class Player(GObject.GObject):
bus.connect("message::tag", self.__msg_tags)
self.bind_property("volume", self._playbin, "volume")
self.bind_property("bg-enabled", self._filter, "bg-enabled")
self.bind_property("bg-volume", self._filter, "bg-volume")
self.connect("notify::file", self.__notify_file)
@ -163,7 +167,7 @@ class Player(GObject.GObject):
def get_replaygain(self) -> tuple[bool, str | None]:
"""Get the current ReplayGain mode."""
mode = self._replaygain.mode
mode = self._filter.rg_mode
return (False, None) if mode == "disabled" else (True, mode)
def get_state(self) -> Gst.State:
@ -191,7 +195,7 @@ class Player(GObject.GObject):
def set_replaygain(self, enabled: bool, mode: str) -> None:
"""Set the ReplayGain mode."""
self._replaygain.mode = mode if enabled else "disabled"
self._filter.rg_mode = mode if enabled else "disabled"
def set_state_sync(self, state: Gst.State) -> None:
"""Set the state of the playbin, and wait for it to change."""

45
emmental/audio/filter.py Normal file
View File

@ -0,0 +1,45 @@
# Copyright 2023 (c) Anna Schumaker.
"""A custom Gst.Bin with our audio filter effects."""
from gi.repository import GObject
from gi.repository import Gst
from . import replaygain
class Filter(Gst.Bin):
"""The audio filter element."""
bg_enabled = GObject.Property(type=bool, default=False)
bg_volume = GObject.Property(type=float, default=0.5)
rg_mode = GObject.Property(type=str, default="disabled")
def __init__(self):
"""Initialize the audio filter."""
super().__init__()
self._replaygain = replaygain.Filter()
self._volume = Gst.ElementFactory.make("volume")
self.add(self._replaygain)
self.add(self._volume)
rg_pad = self._replaygain.get_static_pad("src")
rg_pad.link(self._volume.get_static_pad("sink"))
self.__add_ghost_pad("sink", self._replaygain)
self.__add_ghost_pad("src", self._volume)
self.connect("notify", self.__notify)
def __add_ghost_pad(self, pad: str, elm: Gst.Element) -> None:
self.add_pad(Gst.GhostPad.new(pad, elm.get_static_pad(pad)))
def __notify(self, filter: Gst.Bin, param: GObject.ParamSpec) -> None:
match param.name:
case "bg-enabled" | "bg-volume":
vol = self.bg_volume if self.bg_enabled else 1.0
if vol != self._volume.get_property("volume"):
vs = f"{round(vol * 100)}%" if self.bg_enabled else "off"
print(f"audio: setting background listening to {vs}")
self._volume.set_property("volume", vol)
case "rg-mode":
if self.rg_mode != self._replaygain.mode:
self._replaygain.mode = self.rg_mode

View File

@ -40,26 +40,31 @@ def add_style():
CSS_PROVIDER, CSS_PRIORITY)
def __print_version(subsystem, major, minor, micro):
print(f"{subsystem} {major}.{minor}.{micro}")
def __version_string(subsystem, major, minor, micro):
return f"{subsystem} {major}.{minor}.{micro}"
def print_versions():
"""Print version information for libraries we use."""
__print_version("Python", sys.version_info.major, sys.version_info.minor,
sys.version_info.micro)
__print_version("Gtk", gi.repository.Gtk.MAJOR_VERSION,
gi.repository.Gtk.MINOR_VERSION,
gi.repository.Gtk.MICRO_VERSION)
__print_version("Libadwaita", gi.repository.Adw.MAJOR_VERSION,
gi.repository.Adw.MINOR_VERSION,
gi.repository.Adw.MICRO_VERSION)
__print_version("GStreamer", gi.repository.Gst.version().major,
gi.repository.Gst.version().minor,
gi.repository.Gst.version().micro)
__print_version("Pango", gi.repository.Pango.VERSION_MAJOR,
gi.repository.Pango.VERSION_MINOR,
gi.repository.Pango.VERSION_MICRO)
__print_version("SQLite", sqlite3.sqlite_version_info[0],
sqlite3.sqlite_version_info[1],
sqlite3.sqlite_version_info[2])
def env_string() -> str:
"""Return a string with the version numbers of our dependencies."""
gst = gi.repository.Gst.version()
strs = [__version_string("Python", sys.version_info.major,
sys.version_info.minor, sys.version_info.micro),
__version_string("Gtk", gi.repository.Gtk.MAJOR_VERSION,
gi.repository.Gtk.MINOR_VERSION,
gi.repository.Gtk.MICRO_VERSION),
__version_string("Libadwaita", gi.repository.Adw.MAJOR_VERSION,
gi.repository.Adw.MINOR_VERSION,
gi.repository.Adw.MICRO_VERSION),
__version_string("GStreamer", gst.major, gst.minor, gst.micro),
__version_string("Pango", gi.repository.Pango.VERSION_MAJOR,
gi.repository.Pango.VERSION_MINOR,
gi.repository.Pango.VERSION_MICRO),
__version_string("SQLite", sqlite3.sqlite_version_info[0],
sqlite3.sqlite_version_info[1],
sqlite3.sqlite_version_info[2])]
return "\n".join(strs)
def print_env() -> None:
"""Print the environment versions to stdout."""
print(env_string())

View File

@ -1,11 +1,13 @@
# Copyright 2022 (c) Anna Schumaker.
"""A custom Gtk.HeaderBar configured for our application."""
import pathlib
import typing
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Adw
from .. import db
from .. import buttons
from .. import gsetup
from . import open
from . import replaygain
from . import volume
@ -31,6 +33,8 @@ class Header(Gtk.HeaderBar):
sql = GObject.Property(type=db.Connection)
title = GObject.Property(type=str)
subtitle = GObject.Property(type=str)
bg_enabled = GObject.Property(type=bool, default=False)
bg_volume = GObject.Property(type=float, default=0.5)
rg_enabled = GObject.Property(type=bool, default=False)
rg_mode = GObject.Property(type=str, default="auto")
volume = GObject.Property(type=float, default=1.0)
@ -39,21 +43,34 @@ class Header(Gtk.HeaderBar):
"""Initialize the HeaderBar."""
super().__init__(title=title, subtitle=SUBTITLE, sql=sql)
self._open = open.Button()
self._title = Adw.WindowTitle(title=self.title, subtitle=self.subtitle)
self._volume = volume.Controls()
self._replaygain = replaygain.Selector()
self._title = Adw.WindowTitle(title=self.title, subtitle=self.subtitle,
tooltip_text=gsetup.env_string())
self._volume = volume.VolumeRow()
self._volume_icon = Gtk.Image(icon_name=_volume_icon(self.volume))
self._background = volume.BackgroundRow()
self._background_icon = Gtk.Image(icon_name="sound-wave")
self._replaygain = replaygain.ReplayGainRow()
self._box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
self._icons = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 6)
self._icons.append(self._volume_icon)
self._icons.append(self._background_icon)
self._box = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
self._box.add_css_class("boxed-list")
self._box.append(self._volume)
self._box.append(Gtk.Separator())
self._box.append(self._background)
self._box.append(self._replaygain)
icon = _volume_icon(self.volume)
self._button = buttons.PopoverButton(popover_child=self._box,
icon_name=icon)
child=self._icons,
has_frame=False, margin_end=6)
self.bind_property("title", self._title, "title")
self.bind_property("subtitle", self._title, "subtitle")
self.bind_property("bg-enabled", self._background, "enabled",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("bg-volume", self._background, "volume",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("rg-enabled", self._replaygain, "enabled",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("rg-mode", self._replaygain, "mode",
@ -64,7 +81,8 @@ class Header(Gtk.HeaderBar):
self.pack_start(self._open)
if __debug__:
self._window = settings.Window(sql)
self._settings = Gtk.Button.new_from_icon_name("settings-symbolic")
self._settings = Gtk.Button(icon_name="settings-symbolic",
tooltip_text="open settings editor")
self._settings.connect("clicked", self.__run_settings)
self.pack_start(self._settings)
@ -72,14 +90,29 @@ class Header(Gtk.HeaderBar):
self.set_title_widget(self._title)
self._open.connect("track-requested", self.__track_requested)
self.connect("notify::volume", self.__notify_volume)
self.connect("notify", self.__notify)
def __run_settings(self, button: Gtk.Button) -> None:
if __debug__:
self._window.present()
def __notify_volume(self, header, param) -> None:
self._button.set_icon_name(_volume_icon(self.volume))
def __notify(self, header: typing.Self, param: GObject.ParamSpec) -> None:
match param.name:
case "bg-enabled":
icon = "sound-wave-alt" if self.bg_enabled else "sound-wave"
self._background_icon.set_from_icon_name(icon)
case "volume":
self._volume_icon.set_from_icon_name(_volume_icon(self.volume))
bg_status = "off"
if self.bg_enabled:
bg_status = f"{round(self.bg_volume * 100)}%"
rg_status = f"{self.rg_mode} mode" if self.rg_enabled else "off"
status = (f"volume: {round(self.volume * 100)}%\n"
f"background listening: {bg_status}\n"
f"normalizing: {rg_status}")
self._button.set_tooltip_text(status)
def __track_requested(self, button: open.Button,
path: pathlib.Path) -> None:

View File

@ -12,7 +12,8 @@ class Button(Gtk.Button):
def __init__(self):
"""Initialize our open button."""
super().__init__(icon_name="document-open-symbolic")
super().__init__(icon_name="document-open-symbolic",
tooltip_text="open a file for playback")
self._filters = Gio.ListStore()
self._filter = Gtk.FileFilter(name="Audio Files",
mime_types=["inode/directory",

View File

@ -2,9 +2,36 @@
"""A widget for selecting ReplayGain mode."""
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Adw
class Selector(Gtk.Grid):
class CheckRow(Adw.ActionRow):
"""A custom Adw.ActionRow displaying a Check Button."""
active = GObject.Property(type=bool, default=False)
group = GObject.Property(type=Adw.ActionRow)
mode = GObject.Property(type=str)
def __init__(self, mode: str, active: bool = False,
group: Adw.ActionRow | None = None, **kwargs):
"""Initialize the Check Row."""
super().__init__(mode=mode, active=active, group=group, **kwargs)
self._prefix = Gtk.CheckButton(active=active,
group=group._prefix if group else None)
self.bind_property("active", self._prefix, "active",
GObject.BindingFlags.BIDIRECTIONAL)
self.set_activatable_widget(self._prefix)
self.add_prefix(self._prefix)
def set_active(self, newval: bool) -> None:
"""Set the active property."""
if self.active != newval:
self.active = newval
class ReplayGainRow(Adw.ExpanderRow):
"""Build up a widget for configuring ReplayGain settings."""
enabled = GObject.Property(type=bool, default=False)
@ -12,42 +39,40 @@ class Selector(Gtk.Grid):
def __init__(self):
"""Initialize the ReplayGain selector."""
super().__init__(column_spacing=6, margin_top=8)
self._title = Gtk.Label(label="Volume Normalization", yalign=0.8,
hexpand=True, vexpand=True)
self._switch = Gtk.Switch()
self._auto = Gtk.CheckButton(label="Decide automatically",
sensitive=False, active=True)
self._album = Gtk.CheckButton(label="Albums have the same volume",
sensitive=False, group=self._auto)
self._track = Gtk.CheckButton(label="Tracks have the same volume",
sensitive=False, group=self._auto)
super().__init__(title="Volume Normalization",
subtitle="Configure ReplayGain normalizing")
self._switch = Gtk.Switch(valign=Gtk.Align.CENTER)
self._automatic = CheckRow(title="Automatic Mode",
subtitle="Emmental decides automatically",
mode="auto", active=True)
self._album = CheckRow(title="Album Mode",
subtitle="Albums have the same volume",
mode="album", group=self._automatic)
self._track = CheckRow(title="Track Mode",
subtitle="Tracks have the same volume",
mode="track", group=self._automatic)
self.attach(self._title, 0, 0, 1, 1)
self.attach(self._switch, 1, 0, 1, 1)
self.attach(self._auto, 0, 1, 2, 1)
self.attach(self._album, 0, 2, 2, 1)
self.attach(self._track, 0, 3, 2, 1)
self.add_prefix(self._switch)
self.add_row(self._automatic)
self.add_row(self._album)
self.add_row(self._track)
self.connect("notify::mode", self.__notify_mode)
self._auto.connect("toggled", self.__mode_toggled, "auto")
self._album.connect("toggled", self.__mode_toggled, "album")
self._track.connect("toggled", self.__mode_toggled, "track")
self._automatic.connect("notify::active", self.__row_activated)
self._album.connect("notify::active", self.__row_activated)
self._track.connect("notify::active", self.__row_activated)
self._switch.bind_property("state", self._auto, "sensitive")
self._switch.bind_property("state", self._album, "sensitive")
self._switch.bind_property("state", self._track, "sensitive")
self.bind_property("enabled", self._switch, "state",
self._switch.bind_property("active", self, "expanded",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("enabled", self._switch, "active",
GObject.BindingFlags.BIDIRECTIONAL)
self._title.add_css_class("title-4")
def __notify_mode(self, selector: Gtk.Grid, param) -> None:
match selector.get_property("mode"):
def __notify_mode(self, row: Adw.ExpanderRow, param) -> None:
match self.mode:
case "album": self._album.set_active(True)
case "track": self._track.set_active(True)
case _: self._auto.set_active(True)
case _: self._automatic.set_active(True)
def __mode_toggled(self, check: Gtk.CheckButton, new_mode: str) -> None:
if check.get_active():
self.mode = new_mode
def __row_activated(self, row: CheckRow, param: GObject.ParamSpec) -> None:
if row.active:
self.mode = row.mode

View File

@ -2,6 +2,7 @@
"""A custom Gtk.Box with controls for adjusting the volume."""
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Adw
STEP_SIZE = 0.05
@ -11,29 +12,34 @@ def format_value_func(scale, value: float) -> str:
return f"{round(value*100)} %"
class Controls(Gtk.Box):
class VolumeRow(Gtk.ListBoxRow):
"""A Gtk.Box containing widgets for adjusting the volume."""
volume = GObject.Property(type=float, default=1.0)
def __init__(self):
def __init__(self, volume: float = 1.0):
"""Initialize our volume controls."""
super().__init__()
super().__init__(volume=volume)
self._box = Gtk.Box()
self._decrement = Gtk.Button(icon_name="list-remove-symbolic",
tooltip_text="reduce the volume",
valign=Gtk.Align.END, has_frame=False,
margin_bottom=6)
self._adjustment = Gtk.Adjustment.new(1.0, 0.0, 1.0, STEP_SIZE, 0, 0)
margin_bottom=5)
self._adjustment = Gtk.Adjustment.new(volume, 0.0, 1.0,
STEP_SIZE, 0, 0)
self._scale = Gtk.Scale(adjustment=self._adjustment, draw_value=True,
valign=Gtk.Align.END, hexpand=True)
self._increment = Gtk.Button(icon_name="list-add-symbolic",
tooltip_text="increase the volume",
valign=Gtk.Align.END, has_frame=False,
margin_bottom=6)
margin_bottom=5)
self._scale.set_format_value_func(format_value_func)
self.append(self._decrement)
self.append(self._scale)
self.append(self._increment)
self._box.append(self._decrement)
self._box.append(self._scale)
self._box.append(self._increment)
self.set_child(self._box)
self._decrement.connect("clicked", self.__decrement)
self._scale.connect("value-changed", self.__value_changed)
@ -50,3 +56,27 @@ class Controls(Gtk.Box):
def __value_changed(self, range: Gtk.Range) -> None:
self.volume = range.get_value()
class BackgroundRow(Adw.ExpanderRow):
"""A VolumeRow for setting Background Listening volume."""
enabled = GObject.Property(type=bool, default=False)
volume = GObject.Property(type=float, default=0.5)
def __init__(self):
"""Initialize the BackgroundRow."""
super().__init__(title="Background Listening",
subtitle="Decrease the volume to help focus")
self._switch = Gtk.Switch(valign=Gtk.Align.CENTER)
self._volume = VolumeRow(volume=self.volume)
self.add_prefix(self._switch)
self.add_row(self._volume)
self._switch.bind_property("active", self, "expanded",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("enabled", self._switch, "active",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("volume", self._volume, "volume",
GObject.BindingFlags.BIDIRECTIONAL)

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 5.5 4 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 8 c 0 0.277344 0.222656 0.5 0.5 0.5 s 0.5 -0.222656 0.5 -0.5 v -8 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 4 1 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 6 c 0 0.277344 0.222656 0.5 0.5 0.5 s 0.5 -0.222656 0.5 -0.5 v -6 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m -2 1 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 4 c 0 0.277344 0.222656 0.5 0.5 0.5 s 0.5 -0.222656 0.5 -0.5 v -4 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 4 0 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 4 c 0 0.277344 0.222656 0.5 0.5 0.5 s 0.5 -0.222656 0.5 -0.5 v -4 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m -8 1 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 3 c 0 0.277344 0.222656 0.5 0.5 0.5 s 0.5 -0.222656 0.5 -0.5 v -3 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m -2.027344 1 c -0.261718 0.011719 -0.472656 0.230469 -0.472656 0.5 v 1 c 0 0.277344 0.222656 0.5 0.5 0.5 s 0.5 -0.222656 0.5 -0.5 v -1 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 c -0.007812 0 -0.015625 0 -0.027344 0 z m 12 0 c -0.261718 0.011719 -0.472656 0.230469 -0.472656 0.5 v 1 c 0 0.277344 0.222656 0.5 0.5 0.5 s 0.5 -0.222656 0.5 -0.5 v -1 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 c -0.007812 0 -0.015625 0 -0.027344 0 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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 8 2 c -0.554688 0 -1 0.445312 -1 1 v 10 c 0 0.554688 0.445312 1 1 1 s 1 -0.445312 1 -1 v -10 c 0 -0.554688 -0.445312 -1 -1 -1 z m 6 2 c -0.554688 0 -1 0.445312 -1 1 v 6 c 0 0.554688 0.445312 1 1 1 s 1 -0.445312 1 -1 v -6 c 0 -0.554688 -0.445312 -1 -1 -1 z m -9 1 c -0.554688 0 -1 0.445312 -1 1 v 4 c 0 0.554688 0.445312 1 1 1 s 1 -0.445312 1 -1 v -4 c 0 -0.554688 -0.445312 -1 -1 -1 z m 6 1 c -0.554688 0 -1 0.445312 -1 1 v 2 c 0 0.554688 0.445312 1 1 1 s 1 -0.445312 1 -1 v -2 c 0 -0.554688 -0.445312 -1 -1 -1 z m -9 1 c -0.554688 0 -1 0.445312 -1 1 s 0.445312 1 1 1 s 1 -0.445312 1 -1 s -0.445312 -1 -1 -1 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 762 B

View File

@ -53,6 +53,27 @@ class TestAudio(unittest.TestCase):
self.assertRegex(self.player._playbin.get_property("video-sink").name,
r"fakesink\d+")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_filter(self, mock_stdout: io.StringIO):
"""Test the filter element added to the playbin."""
self.assertIsInstance(self.player._filter,
emmental.audio.filter.Filter)
self.assertEqual(self.player._playbin.get_property("audio-filter"),
self.player._filter)
self.assertFalse(self.player.bg_enabled)
self.assertEqual(self.player.bg_volume, 0.5)
self.player.bg_enabled = True
self.player.bg_volume = 0.75
self.assertTrue(self.player._filter.bg_enabled)
self.assertEqual(self.player._filter.bg_volume, 0.75)
self.player.bg_enabled = False
self.player.bg_volume = 0.5
self.assertFalse(self.player._filter.bg_enabled)
self.assertEqual(self.player.bg_volume, 0.5)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_eos(self, mock_stdout: io.StringIO):
"""Test handling an EOS message."""
@ -297,22 +318,17 @@ class TestAudio(unittest.TestCase):
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_replaygain(self, mock_stdout: io.StringIO):
"""Test that ReplayGain functions work as expected."""
self.assertIsInstance(self.player._replaygain,
emmental.audio.replaygain.Filter)
self.assertEqual(self.player._playbin.get_property("audio-filter"),
self.player._replaygain)
self.assertEqual(self.player._replaygain.mode, "disabled")
self.assertEqual(self.player._filter.rg_mode, "disabled")
self.assertEqual(self.player.get_replaygain(), (False, None))
self.player.set_replaygain(True, "album")
self.assertEqual(self.player._replaygain.mode, "album")
self.assertEqual(self.player._filter.rg_mode, "album")
self.assertEqual(self.player.get_replaygain(), (True, "album"))
self.assertRegex(mock_stdout.getvalue(),
r"audio: setting ReplayGain mode to 'album'")
self.player.set_replaygain(False, "track")
self.assertEqual(self.player._replaygain.mode, "disabled")
self.assertEqual(self.player._filter.rg_mode, "disabled")
self.assertEqual(self.player.get_replaygain(), (False, None))
self.assertRegex(mock_stdout.getvalue(),
r"audio: setting ReplayGain mode to 'disabled'")

View File

@ -0,0 +1,72 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our combined Filter element."""
import io
import unittest
import emmental.audio.filter
from gi.repository import Gst
class TestFilter(unittest.TestCase):
"""Tests our custom Filter element."""
def setUp(self):
"""Set up common variables."""
self.filter = emmental.audio.filter.Filter()
def test_init(self):
"""Test that the filter is set up correctly."""
self.assertIsInstance(self.filter, Gst.Bin)
self.assertIsInstance(self.filter.get_static_pad("src"), Gst.GhostPad)
self.assertIsInstance(self.filter.get_static_pad("sink"), Gst.GhostPad)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_replaygain(self, mock_stdout: io.StringIO):
"""Test the ReplayGain element in the filter."""
self.assertIsInstance(self.filter._replaygain,
emmental.audio.replaygain.Filter)
self.assertEqual(self.filter.rg_mode, "disabled")
self.filter.rg_mode = "track"
self.assertEqual(self.filter._replaygain.mode, "track")
self.filter.rg_mode = "track"
self.assertEqual(mock_stdout.getvalue(),
"audio: setting ReplayGain mode to 'track'\n")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_volume(self, mock_stdout: io.StringIO):
"""Test the Volume element in the filter."""
self.assertIsInstance(self.filter._volume, Gst.Element)
self.assertRegex(self.filter._volume.name, r"volume\d+")
self.assertEqual(self.filter._volume.get_property("volume"), 1.0)
self.assertFalse(self.filter.bg_enabled)
self.assertEqual(self.filter.bg_volume, 0.5)
self.filter.bg_enabled = True
self.assertEqual(self.filter._volume.get_property("volume"), 0.5)
self.assertEqual(mock_stdout.getvalue(),
"audio: setting background listening to 50%\n")
self.filter.bg_volume = 0.50
self.assertEqual(mock_stdout.getvalue(),
"audio: setting background listening to 50%\n")
self.filter.bg_volume = 0.75
self.assertEqual(self.filter._volume.get_property("volume"), 0.75)
self.assertEqual(mock_stdout.getvalue(),
"audio: setting background listening to 50%\n"
"audio: setting background listening to 75%\n")
self.filter.bg_enabled = False
self.assertEqual(self.filter._volume.get_property("volume"), 1.0)
self.assertEqual(mock_stdout.getvalue(),
"audio: setting background listening to 50%\n"
"audio: setting background listening to 75%\n"
"audio: setting background listening to off\n")
self.filter.bg_volume = 0.5
self.assertEqual(self.filter._volume.get_property("volume"), 1.0)
self.assertEqual(mock_stdout.getvalue(),
"audio: setting background listening to 50%\n"
"audio: setting background listening to 75%\n"
"audio: setting background listening to off\n")

View File

@ -33,6 +33,9 @@ class TestHeader(tests.util.TestCase):
self.assertEqual(self.header._title.get_subtitle(),
emmental.header.SUBTITLE)
self.assertEqual(self.header._title.get_tooltip_text(),
emmental.gsetup.env_string())
def test_open(self):
"""Check that the Open button works as expected."""
self.assertIsInstance(self.header._open, emmental.header.open.Button)
@ -51,16 +54,31 @@ class TestHeader(tests.util.TestCase):
self.assertEqual(self.header.sql, self.sql)
self.assertEqual(self.header._settings.get_icon_name(),
"settings-symbolic")
self.assertEqual(self.header._settings.get_tooltip_text(),
"open settings editor")
with unittest.mock.patch.object(self.header._window,
"present") as mock_present:
self.header._settings.emit("clicked")
mock_present.assert_called()
def test_volume_icons(self):
"""Check that the volume icons box is set up properly."""
self.assertIsInstance(self.header._icons, Gtk.Box)
self.assertIsInstance(self.header._volume_icon, Gtk.Image)
self.assertIsInstance(self.header._background_icon, Gtk.Image)
self.assertEqual(self.header._icons.get_spacing(), 6)
self.assertEqual(self.header._icons.get_first_child(),
self.header._volume_icon)
self.assertEqual(self.header._volume_icon.get_next_sibling(),
self.header._background_icon)
def test_volume(self):
"""Check that volume widgets work as expected."""
self.assertIsInstance(self.header._volume,
emmental.header.volume.Controls)
emmental.header.volume.VolumeRow)
self.assertEqual(self.header.volume, 1.0)
for i, vol in [(x, x/10) for x in range(11)]:
@ -75,13 +93,51 @@ class TestHeader(tests.util.TestCase):
widget.volume = vol
self.assertEqual(self.header.volume, vol)
self.assertEqual(self.header._volume.volume, vol)
self.assertEqual(self.header._button.get_icon_name(),
self.assertEqual(self.header._volume_icon.get_icon_name(),
f"audio-volume-{icon}-symbolic")
self.assertEqual(self.header._button.get_tooltip_text(),
f"volume: {i*10}%\n"
"background listening: off\nnormalizing: off")
def test_background_listening(self):
"""Test the background listening mode."""
self.assertIsInstance(self.header._background,
emmental.header.volume.BackgroundRow)
self.assertEqual(self.header._background_icon.get_icon_name(),
"sound-wave")
self.assertFalse(self.header.bg_enabled)
self.assertEqual(self.header.bg_volume, 0.5)
self.header.bg_enabled = True
self.assertTrue(self.header._background.enabled)
self.assertEqual(self.header._background_icon.get_icon_name(),
"sound-wave-alt")
self.assertEqual(self.header._button.get_tooltip_text(),
"volume: 100%\nbackground listening: 50%\n"
"normalizing: off")
self.header.bg_volume = 0.75
self.assertEqual(self.header._background.volume, 0.75)
self.assertEqual(self.header._button.get_tooltip_text(),
"volume: 100%\nbackground listening: 75%\n"
"normalizing: off")
self.header._background.volume = 0.25
self.assertEqual(self.header.bg_volume, 0.25)
self.assertEqual(self.header._button.get_tooltip_text(),
"volume: 100%\nbackground listening: 25%\n"
"normalizing: off")
self.header._background.enabled = False
self.assertFalse(self.header.bg_enabled)
self.assertEqual(self.header._background_icon.get_icon_name(),
"sound-wave")
def test_replaygain(self):
"""Test that we can configure ReplayGain as expected."""
self.assertIsInstance(self.header._replaygain,
emmental.header.replaygain.Selector)
emmental.header.replaygain.ReplayGainRow)
self.assertFalse(self.header.rg_enabled)
self.assertEqual(self.header.rg_mode, "auto")
@ -89,29 +145,38 @@ class TestHeader(tests.util.TestCase):
self.header.rg_mode = "track"
self.assertTrue(self.header._replaygain.enabled)
self.assertEqual(self.header._replaygain.mode, "track")
self.assertEqual(self.header._button.get_tooltip_text(),
"volume: 100%\nbackground listening: off\n"
"normalizing: track mode")
self.header._replaygain.enabled = False
self.header._replaygain.mode = "album"
self.assertFalse(self.header.rg_enabled)
self.assertEqual(self.header.rg_mode, "album")
self.assertEqual(self.header._button.get_tooltip_text(),
"volume: 100%\nbackground listening: off\n"
"normalizing: off")
def test_popover(self):
"""Check that the menu popover was set up correctly."""
def test_popover_button(self):
"""Check that the menu popover button 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)
sep = self.header._volume.get_next_sibling()
self.assertIsInstance(sep, Gtk.Separator)
self.assertEqual(sep.get_orientation(), Gtk.Orientation.HORIZONTAL)
self.assertEqual(sep.get_next_sibling(), self.header._replaygain)
self.assertEqual(self.header._button.get_child(), self.header._icons)
self.assertEqual(self.header._button.get_margin_end(), 6)
self.assertFalse(self.header._button.get_has_frame())
def test_popover_child(self):
"""Check that the menu popover button child was set up correctly."""
self.assertIsInstance(self.header._box, Gtk.ListBox)
self.assertEqual(self.header._box.get_selection_mode(),
Gtk.SelectionMode.NONE)
self.assertTrue(self.header._box.has_css_class("boxed-list"))
self.assertEqual(self.header._box.get_row_at_index(0),
self.header._volume)
self.assertEqual(self.header._box.get_row_at_index(1),
self.header._background)
self.assertEqual(self.header._box.get_row_at_index(2),
self.header._replaygain)

View File

@ -18,6 +18,8 @@ class TestButton(unittest.TestCase):
"""Check that the button was set up properly."""
self.assertIsInstance(self.button, Gtk.Button)
self.assertEqual(self.button.get_icon_name(), "document-open-symbolic")
self.assertEqual(self.button.get_tooltip_text(),
"open a file for playback")
def test_filter(self):
"""Check that the file filter is set up properly."""

View File

@ -1,119 +1,159 @@
# Copyright 2022 (c) Anna Schumaker
"""Tests our ReplayGain selector."""
import unittest
import unittest.mock
import emmental.header.replaygain
from gi.repository import Gtk
from gi.repository import Adw
class TestSelector(unittest.TestCase):
"""Test case for our custom ReplayGain Selector."""
class TestCheckRow(unittest.TestCase):
"""Test case for our custom CheckRow ListBox row."""
def setUp(self):
"""Set up common variables."""
self.selector = emmental.header.replaygain.Selector()
self.checkrow = emmental.header.replaygain.CheckRow("test mode",
title="My Title")
def test_selector(self):
"""Check that the Selector is set up properly."""
self.assertIsInstance(self.selector, Gtk.Grid)
self.assertEqual(self.selector.get_column_spacing(), 6)
self.assertEqual(self.selector.get_margin_top(), 8)
def test_init(self):
"""Test that the CheckRow is set up properly."""
self.assertIsInstance(self.checkrow, Adw.ActionRow)
self.assertIsInstance(self.checkrow._prefix, Gtk.CheckButton)
self.assertEqual(self.selector.mode, "auto")
self.assertFalse(self.selector.enabled)
self.assertEqual(self.checkrow.mode, "test mode")
self.assertEqual(self.checkrow.get_title(), "My Title")
self.assertEqual(self.checkrow.get_activatable_widget(),
self.checkrow._prefix)
self.selector.enabled = True
self.assertTrue(self.selector._switch.get_state())
self.selector.enabled = False
self.assertFalse(self.selector._switch.get_state())
def test_active(self):
"""Test the CheckRow active property."""
self.assertFalse(self.checkrow.active)
def test_title(self):
"""Check that the Selector title label is set up properly."""
self.assertIsInstance(self.selector._title, Gtk.Label)
self.assertEqual(self.selector.get_child_at(0, 0),
self.selector._title)
self.checkrow.active = True
self.assertTrue(self.checkrow._prefix.get_active())
self.checkrow._prefix.set_active(False)
self.assertFalse(self.checkrow.active)
self.assertEqual(self.selector._title.get_text(),
"Volume Normalization")
self.assertAlmostEqual(self.selector._title.get_yalign(), 0.8)
self.assertTrue(self.selector._title.has_css_class("title-4"))
self.assertTrue(self.selector._title.get_hexpand())
self.assertTrue(self.selector._title.get_vexpand())
checkrow2 = emmental.header.replaygain.CheckRow("other", active=True)
self.assertTrue(checkrow2.active)
self.assertTrue(checkrow2._prefix.get_active())
def test_group(self):
"""Test the CheckRow group property."""
self.assertIsNone(self.checkrow.group)
checkrow2 = emmental.header.replaygain.CheckRow("other",
group=self.checkrow)
self.assertEqual(checkrow2.group, self.checkrow)
def test_set_active(self):
"""Test the set_active() property."""
notify = unittest.mock.Mock()
self.checkrow.connect("notify::active", notify)
self.checkrow.set_active(True)
self.assertTrue(self.checkrow.active)
notify.assert_called()
notify.reset_mock()
self.checkrow.set_active(True)
notify.assert_not_called()
class TestReplayGainRow(unittest.TestCase):
"""Test case for our custom ReplayGain ListBox row."""
def setUp(self):
"""Set up common variables."""
self.replaygain = emmental.header.replaygain.ReplayGainRow()
def test_init(self):
"""Check that the ReplayGainRow is set up properly."""
self.assertIsInstance(self.replaygain, Adw.ExpanderRow)
self.assertEqual(self.replaygain.get_title(), "Volume Normalization")
self.assertEqual(self.replaygain.get_subtitle(),
"Configure ReplayGain normalizing")
self.assertEqual(self.replaygain.mode, "auto")
self.assertFalse(self.replaygain.enabled)
self.replaygain.set_expanded(True)
self.assertTrue(self.replaygain._switch.get_active())
self.replaygain.set_expanded(False)
self.assertFalse(self.replaygain._switch.get_active())
def test_switch(self):
"""Check that the Selector switch works as intended."""
self.assertIsInstance(self.selector._switch, Gtk.Switch)
self.assertEqual(self.selector.get_child_at(1, 0),
self.selector._switch)
self.assertFalse(self.selector._switch.get_active())
"""Check that the ReplayGainRow switch works as intended."""
self.assertIsInstance(self.replaygain._switch, Gtk.Switch)
self.assertEqual(self.replaygain._switch.get_valign(),
Gtk.Align.CENTER)
self.assertFalse(self.replaygain._switch.get_active())
self.selector._switch.set_active(True)
self.assertTrue(self.selector.enabled)
self.assertTrue(self.selector._auto.get_sensitive())
self.assertTrue(self.selector._album.get_sensitive())
self.assertTrue(self.selector._track.get_sensitive())
self.selector._switch.set_active(False)
self.assertFalse(self.selector.enabled)
self.assertFalse(self.selector._auto.get_sensitive())
self.assertFalse(self.selector._album.get_sensitive())
self.assertFalse(self.selector._track.get_sensitive())
self.replaygain._switch.set_active(True)
self.assertTrue(self.replaygain.get_expanded())
self.replaygain._switch.set_active(False)
self.assertFalse(self.replaygain.get_expanded())
def test_automatic_mode(self):
"""Test the Selector automatic mode button."""
self.assertIsInstance(self.selector._auto, Gtk.CheckButton)
self.assertEqual(self.selector.get_child_at(0, 1), self.selector._auto)
"""Test the ReplayGainRow automatic mode option."""
self.assertIsInstance(self.replaygain._automatic,
emmental.header.replaygain.CheckRow)
self.assertEqual(self.replaygain._automatic.get_title(),
"Automatic Mode")
self.assertEqual(self.replaygain._automatic.get_subtitle(),
"Emmental decides automatically")
self.assertEqual(self.selector._auto.get_label(),
"Decide automatically")
self.assertFalse(self.selector._auto.get_sensitive())
self.assertTrue(self.selector._auto.get_active())
self.assertEqual(self.replaygain._automatic.mode, "auto")
self.assertTrue(self.replaygain._automatic.active)
self.selector._track.set_active(True)
self.selector._auto.set_active(True)
self.assertEqual(self.selector.mode, "auto")
self.replaygain._track.active = True
self.replaygain._automatic.active = True
self.assertEqual(self.replaygain.mode, "auto")
self.assertFalse(self.replaygain._track.active)
def test_album_mode(self):
"""Test the Selector album mode button."""
self.assertIsInstance(self.selector._album, Gtk.CheckButton)
self.assertEqual(self.selector.get_child_at(0, 2),
self.selector._album)
self.assertEqual(self.selector._album.get_label(),
"""Test the ReplayGainRow album mode option."""
self.assertIsInstance(self.replaygain._album,
emmental.header.replaygain.CheckRow)
self.assertEqual(self.replaygain._album.get_title(), "Album Mode")
self.assertEqual(self.replaygain._album.get_subtitle(),
"Albums have the same volume")
self.assertFalse(self.selector._album.get_sensitive())
self.assertFalse(self.selector._album.get_active())
self.selector._album.set_active(True)
self.assertEqual(self.selector.mode, "album")
self.assertEqual(self.replaygain._album.mode, "album")
self.assertEqual(self.replaygain._album.group,
self.replaygain._automatic)
self.replaygain._album.active = True
self.assertEqual(self.replaygain.mode, "album")
def test_track_mode(self):
"""Test the Selector album mode button."""
self.assertIsInstance(self.selector._track, Gtk.CheckButton)
self.assertEqual(self.selector.get_child_at(0, 3),
self.selector._track)
self.assertEqual(self.selector._track.get_label(),
"""Test the ReplayGainRow track mode option."""
self.assertIsInstance(self.replaygain._track,
emmental.header.replaygain.CheckRow)
self.assertEqual(self.replaygain._track.get_title(), "Track Mode")
self.assertEqual(self.replaygain._track.get_subtitle(),
"Tracks have the same volume")
self.assertFalse(self.selector._track.get_sensitive())
self.assertFalse(self.selector._track.get_active())
self.selector._track.set_active(True)
self.assertEqual(self.selector.mode, "track")
self.assertEqual(self.replaygain._track.mode, "track")
self.assertEqual(self.replaygain._track.group,
self.replaygain._automatic)
self.replaygain._track.active = True
self.assertEqual(self.replaygain.mode, "track")
def test_mode_property(self):
"""Test that the mode property is set correctly."""
self.selector.mode = "album"
self.assertTrue(self.selector._album.get_active())
self.assertFalse(self.selector._auto.get_active())
self.assertFalse(self.selector._track.get_active())
self.replaygain.mode = "album"
self.assertTrue(self.replaygain._album.active)
self.assertFalse(self.replaygain._automatic.active)
self.assertFalse(self.replaygain._track.active)
self.selector.mode = "track"
self.assertTrue(self.selector._track.get_active())
self.assertFalse(self.selector._auto.get_active())
self.assertFalse(self.selector._album.get_active())
self.replaygain.mode = "track"
self.assertTrue(self.replaygain._track.active)
self.assertFalse(self.replaygain._automatic.active)
self.assertFalse(self.replaygain._album.active)
self.selector.mode = "anything else"
self.assertTrue(self.selector._auto.get_active())
self.assertFalse(self.selector._album.get_active())
self.assertFalse(self.selector._track.get_active())
self.assertEqual(self.selector.mode, "auto")
self.replaygain.mode = "anything else"
self.assertTrue(self.replaygain._automatic.active)
self.assertFalse(self.replaygain._album.active)
self.assertFalse(self.replaygain._track.active)
self.assertEqual(self.replaygain.mode, "auto")

View File

@ -4,14 +4,15 @@ import unittest
import emmental.header.volume
import tests.util
from gi.repository import Gtk
from gi.repository import Adw
class TestControls(unittest.TestCase):
class TestVolumeRow(unittest.TestCase):
"""Test case for our custom volume controls."""
def setUp(self):
"""Set up common variables."""
self.vol = emmental.header.volume.Controls()
self.vol = emmental.header.volume.VolumeRow()
self.value = tests.util.FloatObject(value=1.0)
self.vol.bind_property("volume", self.value, "value")
@ -21,9 +22,12 @@ class TestControls(unittest.TestCase):
def test_volume(self):
"""Check that the volume Controls are set up properly."""
self.assertIsInstance(self.vol, Gtk.Box)
self.assertEqual(self.vol.get_spacing(), 0)
self.assertEqual(self.vol.get_orientation(),
self.assertIsInstance(self.vol, Gtk.ListBoxRow)
self.assertIsInstance(self.vol._box, Gtk.Box)
self.assertEqual(self.vol.get_child(), self.vol._box)
self.assertEqual(self.vol._box.get_spacing(), 0)
self.assertEqual(self.vol._box.get_orientation(),
Gtk.Orientation.HORIZONTAL)
self.assertEqual(self.vol.volume, 1.0)
@ -31,15 +35,21 @@ class TestControls(unittest.TestCase):
self.assertAlmostEqual(self.vol._scale.get_value(), 0.85)
self.assertAlmostEqual(self.value.value, 0.85)
vol2 = emmental.header.volume.VolumeRow(volume=0.5)
self.assertEqual(vol2.volume, 0.5)
self.assertEqual(vol2._adjustment.get_value(), 0.5)
def test_decrement_button(self):
"""Test the decrement button."""
self.assertIsInstance(self.vol._decrement, Gtk.Button)
self.assertEqual(self.vol.get_first_child(), self.vol._decrement)
self.assertEqual(self.vol._box.get_first_child(), self.vol._decrement)
self.assertEqual(self.vol._decrement.get_tooltip_text(),
"reduce the volume")
self.assertEqual(self.vol._decrement.get_icon_name(),
"list-remove-symbolic")
self.assertEqual(self.vol._decrement.get_valign(), Gtk.Align.END)
self.assertEqual(self.vol._decrement.get_margin_bottom(), 6)
self.assertEqual(self.vol._decrement.get_margin_bottom(), 5)
self.assertFalse(self.vol._decrement.get_has_frame())
self.vol._decrement.emit("clicked")
@ -79,10 +89,12 @@ class TestControls(unittest.TestCase):
self.assertEqual(self.vol._scale.get_next_sibling(),
self.vol._increment)
self.assertEqual(self.vol._increment.get_tooltip_text(),
"increase the volume")
self.assertEqual(self.vol._increment.get_icon_name(),
"list-add-symbolic")
self.assertEqual(self.vol._increment.get_valign(), Gtk.Align.END)
self.assertEqual(self.vol._increment.get_margin_bottom(), 6)
self.assertEqual(self.vol._increment.get_margin_bottom(), 5)
self.assertFalse(self.vol._increment.get_has_frame())
self.vol.volume = 0.9
@ -98,3 +110,58 @@ class TestControls(unittest.TestCase):
with self.subTest(value=value):
self.assertEqual(format_value(self.vol._scale, value/100),
f"{value} %")
class TestBackgroundRow(unittest.TestCase):
"""Test case for our Background Listening volume row."""
def setUp(self):
"""Set up common variables."""
self.background = emmental.header.volume.BackgroundRow()
def test_init(self):
"""Test that the Background Listening row was set up properly."""
self.assertIsInstance(self.background, Adw.ExpanderRow)
self.assertEqual(self.background.get_title(), "Background Listening")
self.assertEqual(self.background.get_subtitle(),
"Decrease the volume to help focus")
self.background.set_expanded(True)
self.assertTrue(self.background._switch.get_active())
self.background.set_expanded(False)
self.assertFalse(self.background._switch.get_active())
def test_switch(self):
"""Check that the BackgroundRow switch works as intended."""
self.assertIsInstance(self.background._switch, Gtk.Switch)
self.assertEqual(self.background._switch.get_valign(),
Gtk.Align.CENTER)
self.assertFalse(self.background._switch.get_active())
self.background._switch.set_active(True)
self.assertTrue(self.background.get_expanded())
self.background._switch.set_active(False)
self.assertFalse(self.background.get_expanded())
def test_volume(self):
"""Check the VolumeRow instance inside the Expander."""
self.assertIsInstance(self.background._volume,
emmental.header.volume.VolumeRow)
self.assertEqual(self.background.volume, 0.5)
self.assertEqual(self.background._volume.volume, 0.5)
self.background.volume = 0.75
self.assertEqual(self.background._volume.volume, 0.75)
self.background._volume.volume = 0.25
self.assertEqual(self.background.volume, 0.25)
def test_enabled(self):
"""Check the BackgroundRow enabled property."""
self.assertFalse(self.background.enabled)
self.background.enabled = True
self.assertTrue(self.background._switch.get_active())
self.background._switch.set_active(False)
self.assertFalse(self.background.enabled)

View File

@ -220,3 +220,18 @@ class TestEmmental(unittest.TestCase):
self.assertEqual(player.get_replaygain(), (True, "track"))
win.header.rg_mode = "album"
self.assertEqual(player.get_replaygain(), (True, "album"))
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_background_mode(self, mock_stdout: io.StringIO):
"""Test setting background mode."""
self.application.db = emmental.db.Connection()
self.application.factory = emmental.playlist.Factory(
self.application.db)
self.application.player = emmental.audio.Player()
win = self.application.build_window()
player = self.application.player
win.header.bg_enabled = True
win.header.bg_volume = 0.5
self.assertTrue(player.bg_enabled)
self.assertEqual(player.bg_volume, 0.5)

View File

@ -4,6 +4,7 @@ import unittest
import pathlib
import emmental
import gi
import io
import xdg.BaseDirectory
@ -65,3 +66,20 @@ class TestGSetup(unittest.TestCase):
"""Check that the DATA_DIR points to the right place."""
data_path = xdg.BaseDirectory.save_data_path("emmental")
self.assertEqual(emmental.gsetup.DATA_DIR, pathlib.Path(data_path))
def test_env_string(self):
"""Check that the env_string() function works as expected."""
self.assertRegex(emmental.gsetup.env_string(),
r" ⋅ Python \d+\.\d+\.\d+\n"
r" ⋅ Gtk \d+\.\d+\.\d+\n"
r" ⋅ Libadwaita \d+\.\d+\.\d+\n"
r" ⋅ GStreamer \d+\.\d+\.\d+\n"
r" ⋅ Pango \d+\.\d+\.\d+\n"
r" ⋅ SQLite \d+\.\d+\.\d+$")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_print_env(self, mock_stdout: io.StringIO):
"""Check that the print_env() function prints the env_string()."""
emmental.gsetup.print_env()
self.assertEqual(mock_stdout.getvalue(),
emmental.gsetup.env_string() + "\n")

View File

@ -51,6 +51,18 @@ class TestSettings(unittest.TestCase):
self.assertEqual(self.app.build_header().volume, 0.5)
self.assertEqual(self.player.volume, 0.5)
def test_save_background_mode(self, mock_stdout: io.StringIO):
"""Check saving and loading background mode from the database."""
self.assertFalse(self.settings["audio.background.enabled"])
self.assertEqual(self.settings["audio.background.volume"], 0.5)
self.win.header.bg_enabled = True
self.win.header.bg_volume = 0.75
win = self.app.build_window()
self.assertTrue(win.header.bg_enabled)
self.assertEqual(win.header.bg_volume, 0.75)
def test_save_replaygain(self, mock_stdout: io.StringIO):
"""Check saving and loading replaygain state from the database."""
self.assertFalse(self.settings["audio.replaygain.enabled"])