diff --git a/emmental/nowplaying/seeker.py b/emmental/nowplaying/seeker.py new file mode 100644 index 0000000..e2b6d42 --- /dev/null +++ b/emmental/nowplaying/seeker.py @@ -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) diff --git a/tests/nowplaying/test_seeker.py b/tests/nowplaying/test_seeker.py new file mode 100644 index 0000000..f62a5b8 --- /dev/null +++ b/tests/nowplaying/test_seeker.py @@ -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)