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:
parent
e0becbb059
commit
2ff03bba18
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue