nowplaying: Create a Seeker

This is a Gtk.Scale configured to be used to display track progress and
seek inside the track.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-07-14 11:41:47 -04:00
parent e0becbb059
commit 2ff03bba18
2 changed files with 126 additions and 0 deletions

View File

@ -0,0 +1,50 @@
# Copyright 2022 (c) Anna Schumaker.
"""A Gtk.Scale configured for position tracking seeking."""
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gst
class Scale(Gtk.Scale):
"""A Gtk.Scale configured for our application."""
def __init__(self, **kwargs):
"""Initialize our Scale."""
super().__init__(margin_start=45, margin_end=45, draw_value=True,
hexpand=True, **kwargs)
self._adjustment = Gtk.Adjustment.new(value=0, lower=0, upper=1,
step_increment=5*Gst.SECOND,
page_increment=30*Gst.SECOND,
page_size=0)
self.set_adjustment(self._adjustment)
self.set_format_value_func(self.format_value)
def format_value(self, scale: Gtk.Scale, value: float) -> str:
"""Format the position and duration values."""
duration = round(self.duration * Gst.USECOND / Gst.SECOND)
position = round(value * Gst.USECOND / Gst.SECOND)
remaining = duration - position
(p_m, p_s) = divmod(position, 60)
(r_m, r_s) = divmod(remaining, 60)
return f"{p_m:02}:{p_s:02} / {r_m:02}:{r_s:02}"
@GObject.Property(type=float)
def duration(self) -> float:
"""Get the duration of the current track."""
return self._adjustment.get_upper()
@duration.setter
def duration(self, newval: float) -> None:
"""Set the duration of the current track."""
self.set_range(0, max(newval, 1))
self.emit("value-changed")
@GObject.Property(type=float)
def position(self) -> float:
"""Get the position of the current track."""
return self.get_value()
@position.setter
def position(self, newval: float) -> None:
"""Set the position of the current track."""
self.set_value(newval)

View File

@ -0,0 +1,76 @@
# Copyright 2022 (c) Anna Schumaker.
"""Test our modified Gtk.Scale."""
import unittest
import emmental.nowplaying.seeker
from gi.repository import Gtk
from gi.repository import Gst
class TestScale(unittest.TestCase):
"""Tests our custom Gtk.Scale."""
def setUp(self):
"""Set up common variables."""
self.scale = emmental.nowplaying.seeker.Scale()
def test_init(self):
"""Test that the scale is configured correctly."""
self.assertIsInstance(self.scale, Gtk.Scale)
self.assertEqual(self.scale.get_margin_start(), 45)
self.assertEqual(self.scale.get_margin_end(), 45)
self.assertEqual(self.scale.get_orientation(),
Gtk.Orientation.HORIZONTAL)
self.assertTrue(self.scale.get_draw_value())
self.assertTrue(self.scale.get_hexpand())
def test_adjustment(self):
"""Test the underlying Gtk.Adjustment."""
self.assertIsInstance(self.scale._adjustment, Gtk.Adjustment)
self.assertEqual(self.scale.get_adjustment(), self.scale._adjustment)
self.assertEqual(self.scale._adjustment.get_value(), 0)
self.assertEqual(self.scale._adjustment.get_lower(), 0)
self.assertEqual(self.scale._adjustment.get_upper(), 1)
self.assertEqual(self.scale._adjustment.get_step_increment(),
5 * Gst.SECOND)
self.assertEqual(self.scale._adjustment.get_page_increment(),
30 * Gst.SECOND)
self.assertEqual(self.scale._adjustment.get_page_size(), 0)
def test_duration(self):
"""Test the duration property."""
self.assertEqual(self.scale.duration, 1)
changed = unittest.mock.Mock()
self.scale.connect("value-changed", changed)
for (duration, expected) in [(5, 5), (10, 10), (0, 1)]:
with self.subTest(duration=duration, expected=expected):
changed.reset_mock()
self.scale.duration = duration
self.assertEqual(self.scale._adjustment.get_upper(), expected)
self.assertEqual(self.scale.duration, expected)
changed.assert_called_with(self.scale)
def test_position(self):
"""Test the position property."""
self.assertEqual(self.scale.position, 0)
self.scale.duration = 10
for pos in range(12):
with self.subTest(pos=pos):
self.scale.position = pos
self.assertEqual(self.scale.get_value(), min(10, pos))
self.assertEqual(self.scale.position, min(10, pos))
def test_format_value(self):
"""Test that the value is formatted correctly."""
for (pos, dur, text) in [(0, 0, "00:00 / 00:00"),
(10, 75, "00:10 / 01:05"),
(75, 720, "01:15 / 10:45"),
(660, 720, "11:00 / 01:00")]:
with self.subTest(pos=pos, dur=dur, text=text):
self.scale.duration = dur * Gst.SECOND / Gst.USECOND
position = pos * Gst.SECOND / Gst.USECOND
output = self.scale.format_value(self.scale, position)
self.assertEqual(output, text)