nowplaying: Create an Autopause Entry
This entry is inspired by the Gtk.SpinButton, but lets us set placeholder text to display the current autopause value to the user. Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
69c59438c2
commit
7cd77d3aed
|
@ -0,0 +1,89 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Widgets for configuring autopause."""
|
||||
import re
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class Entry(Gtk.Entry):
|
||||
"""A custom SpinButton so we can format output in Python."""
|
||||
|
||||
value = GObject.Property(type=int, default=-1, minimum=-1, maximum=99)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a Spin button."""
|
||||
super().__init__(max_width_chars=20, placeholder_text="Keep playing",
|
||||
primary_icon_sensitive=False,
|
||||
primary_icon_name="list-remove-symbolic",
|
||||
secondary_icon_name="list-add-symbolic")
|
||||
self._timeout = (None, None)
|
||||
#
|
||||
self.connect("activate", self.__parse_text)
|
||||
self.connect("icon_press", self.__icon_press)
|
||||
self.connect("icon_release", self.__icon_release)
|
||||
self.connect("notify::value", self.__update_text)
|
||||
|
||||
def __set_value(self, newval: int) -> bool:
|
||||
if -1 <= newval <= 99:
|
||||
self.value = newval
|
||||
return True
|
||||
return False
|
||||
|
||||
def __parse_text(self, entry: Gtk.Entry) -> None:
|
||||
if parse := re.search(r"this|next|cancel|-?\d+",
|
||||
entry.get_text(), re.I):
|
||||
match parse.group().lower():
|
||||
case "cancel": self.__set_value(-1)
|
||||
case "this": self.__set_value(0)
|
||||
case "next": self.__set_value(1)
|
||||
case _: self.__set_value(int(parse.group()))
|
||||
self.delete_text(0, -1)
|
||||
|
||||
def __change_value(self, change_how: str) -> bool:
|
||||
match change_how:
|
||||
case "increment": status = self.__set_value(self.value + 1)
|
||||
case "decrement": status = self.__set_value(self.value - 1)
|
||||
|
||||
if not status:
|
||||
self._timeout = (None, None)
|
||||
elif self._timeout[1] == 150:
|
||||
return GLib.SOURCE_CONTINUE
|
||||
else:
|
||||
timeout_id = GLib.timeout_add(150, self.__change_value, change_how)
|
||||
self._timeout = (timeout_id, 150)
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
def __icon_press(self, entry: Gtk.Entry,
|
||||
icon_pos: Gtk.EntryIconPosition) -> None:
|
||||
self.__icon_release(entry, icon_pos)
|
||||
|
||||
match icon_pos:
|
||||
case Gtk.EntryIconPosition.SECONDARY:
|
||||
change_how = "increment"
|
||||
self.value += 1
|
||||
case Gtk.EntryIconPosition.PRIMARY:
|
||||
change_how = "decrement"
|
||||
self.value -= 1
|
||||
|
||||
timeout_id = GLib.timeout_add(500, self.__change_value, change_how)
|
||||
self._timeout = (timeout_id, 500)
|
||||
|
||||
def __icon_release(self, entry: Gtk.Entry,
|
||||
icon_pos: Gtk.EntryIconPosition) -> None:
|
||||
if self._timeout != (None, None):
|
||||
GLib.source_remove(self._timeout[0])
|
||||
self._timeout = (None, None)
|
||||
|
||||
def __update_text(self, spin, param) -> None:
|
||||
match self.value:
|
||||
case -1: text = "Keep playing"
|
||||
case 0: text = "Pause after this track"
|
||||
case 1: text = "Pause after the next track"
|
||||
case _: text = f"Pause after {self.value} tracks"
|
||||
|
||||
self.set_placeholder_text(text)
|
||||
self.set_icon_sensitive(Gtk.EntryIconPosition.PRIMARY,
|
||||
self.value > -1)
|
||||
self.set_icon_sensitive(Gtk.EntryIconPosition.SECONDARY,
|
||||
self.value < 99)
|
|
@ -0,0 +1,148 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Tests our autopause widgets."""
|
||||
import unittest
|
||||
import emmental.nowplaying.autopause
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
class TestAutopauseEntry(unittest.TestCase):
|
||||
"""Test our custom Autopause Entry."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
self.entry = emmental.nowplaying.autopause.Entry()
|
||||
self.timeout_func = self.entry._Entry__change_value
|
||||
self.down_icon_pos = Gtk.EntryIconPosition.PRIMARY
|
||||
self.up_icon_pos = Gtk.EntryIconPosition.SECONDARY
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
if self.entry._timeout[0] is not None:
|
||||
GLib.source_remove(self.entry._timeout[0])
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the Autopause Entry is configured correctly."""
|
||||
self.assertIsInstance(self.entry, Gtk.Entry)
|
||||
self.assertTupleEqual(self.entry._timeout, (None, None))
|
||||
self.assertEqual(self.entry.get_max_width_chars(), 20)
|
||||
|
||||
def test_placeholder_text(self):
|
||||
"""Test changing the placeholder text with the value."""
|
||||
self.assertEqual(self.entry.value, -1)
|
||||
self.assertEqual(self.entry.get_placeholder_text(), "Keep playing")
|
||||
|
||||
for (value, expected) in [(0, "Pause after this track"),
|
||||
(1, "Pause after the next track"),
|
||||
(99, "Pause after 99 tracks"),
|
||||
(-1, "Keep playing")]:
|
||||
with self.subTest(value=value, expected=expected):
|
||||
self.entry.value = value
|
||||
self.assertEqual(self.entry.get_placeholder_text(), expected)
|
||||
|
||||
@unittest.mock.patch("gi.repository.GLib.source_remove",
|
||||
wraps=GLib.source_remove)
|
||||
@unittest.mock.patch("gi.repository.GLib.timeout_add",
|
||||
wraps=GLib.timeout_add)
|
||||
def test_down_icon(self, mock_tout_add: unittest.mock.Mock,
|
||||
mock_src_remove: unittest.mock.Mock):
|
||||
"""Test that the down icon works as expected."""
|
||||
self.assertEqual(self.entry.get_icon_name(self.down_icon_pos),
|
||||
"list-remove-symbolic")
|
||||
self.assertFalse(self.entry.get_icon_sensitive(self.down_icon_pos))
|
||||
self.assertTrue(self.entry.get_icon_activatable(self.down_icon_pos))
|
||||
|
||||
self.entry.value = 5
|
||||
self.entry.emit("icon-press", self.down_icon_pos)
|
||||
self.assertEqual(self.entry.value, 4)
|
||||
|
||||
mock_tout_add.assert_called_with(500, self.timeout_func, "decrement")
|
||||
self.assertIsNotNone(self.entry._timeout[0])
|
||||
self.assertEqual(self.entry._timeout[1], 500)
|
||||
timeout_id = self.entry._timeout[0]
|
||||
|
||||
self.entry.emit("icon-release", self.down_icon_pos)
|
||||
mock_src_remove.assert_called_with(timeout_id)
|
||||
self.assertTupleEqual(self.entry._timeout, (None, None))
|
||||
|
||||
@unittest.mock.patch("gi.repository.GLib.timeout_add",
|
||||
wraps=GLib.timeout_add)
|
||||
def test_hold_down_icon(self, mock_timeout: unittest.mock.Mock):
|
||||
"""Test holding down the down icon."""
|
||||
self.entry.value = 99
|
||||
self.assertEqual(self.timeout_func("decrement"), GLib.SOURCE_REMOVE)
|
||||
self.assertEqual(self.entry.value, 98)
|
||||
|
||||
mock_timeout.assert_called_with(150, self.timeout_func, "decrement")
|
||||
self.assertTrue(self.entry.get_icon_sensitive(self.down_icon_pos))
|
||||
self.assertIsNotNone(self.entry._timeout[0])
|
||||
self.assertEqual(self.entry._timeout[1], 150)
|
||||
timeout_id = self.entry._timeout[0]
|
||||
|
||||
mock_timeout.reset_mock()
|
||||
self.entry.value = 0
|
||||
self.assertEqual(self.timeout_func("decrement"), GLib.SOURCE_CONTINUE)
|
||||
self.assertFalse(self.entry.get_icon_sensitive(self.down_icon_pos))
|
||||
self.assertEqual(self.entry.value, -1)
|
||||
mock_timeout.assert_not_called()
|
||||
|
||||
self.assertEqual(self.timeout_func("decrement"), GLib.SOURCE_REMOVE)
|
||||
GLib.source_remove(timeout_id)
|
||||
|
||||
@unittest.mock.patch("gi.repository.GLib.source_remove",
|
||||
wraps=GLib.source_remove)
|
||||
@unittest.mock.patch("gi.repository.GLib.timeout_add",
|
||||
wraps=GLib.timeout_add)
|
||||
def test_up_icon(self, mock_tout_add: unittest.mock.Mock,
|
||||
mock_src_remove: unittest.mock.Mock):
|
||||
"""Test that the down icon works as expected."""
|
||||
self.assertEqual(self.entry.get_icon_name(self.up_icon_pos),
|
||||
"list-add-symbolic")
|
||||
self.assertTrue(self.entry.get_icon_sensitive(self.up_icon_pos))
|
||||
self.assertTrue(self.entry.get_icon_activatable(self.up_icon_pos))
|
||||
|
||||
self.entry.emit("icon-press", self.up_icon_pos)
|
||||
self.assertEqual(self.entry.value, 0)
|
||||
|
||||
mock_tout_add.assert_called_with(500, self.timeout_func, "increment")
|
||||
self.assertIsNotNone(self.entry._timeout[0])
|
||||
self.assertEqual(self.entry._timeout[1], 500)
|
||||
timeout_id = self.entry._timeout[0]
|
||||
|
||||
self.entry.emit("icon-release", self.up_icon_pos)
|
||||
self.assertTupleEqual(self.entry._timeout, (None, None))
|
||||
mock_src_remove.assert_called_with(timeout_id)
|
||||
|
||||
@unittest.mock.patch("gi.repository.GLib.timeout_add",
|
||||
wraps=GLib.timeout_add)
|
||||
def test_hold_up_icon(self, mock_timeout: unittest.mock.Mock):
|
||||
"""Test holding down the up icon."""
|
||||
self.assertEqual(self.timeout_func("increment"), GLib.SOURCE_REMOVE)
|
||||
self.assertEqual(self.entry.value, 0)
|
||||
|
||||
mock_timeout.assert_called_with(150, self.timeout_func, "increment")
|
||||
self.assertTrue(self.entry.get_icon_sensitive(self.up_icon_pos))
|
||||
self.assertIsNotNone(self.entry._timeout[0])
|
||||
self.assertEqual(self.entry._timeout[1], 150)
|
||||
timeout_id = self.entry._timeout[0]
|
||||
|
||||
mock_timeout.reset_mock()
|
||||
self.entry.value = 98
|
||||
self.assertEqual(self.timeout_func("increment"), GLib.SOURCE_CONTINUE)
|
||||
self.assertFalse(self.entry.get_icon_sensitive(self.up_icon_pos))
|
||||
self.assertEqual(self.entry.value, 99)
|
||||
mock_timeout.assert_not_called()
|
||||
|
||||
self.assertEqual(self.timeout_func("increment"), GLib.SOURCE_REMOVE)
|
||||
GLib.source_remove(timeout_id)
|
||||
|
||||
def test_parse_text(self):
|
||||
"""Test setting a value through the entry."""
|
||||
subtests = [(str(i), i) for i in [-3, -1, 99, 100]]
|
||||
for (text, value) in [("this", 0), ("NEXT", 1), ("", 1),
|
||||
("", 1), ("cancel", -1)] + subtests:
|
||||
with self.subTest(text=text, value=value):
|
||||
value = min(99, max(-1, value))
|
||||
self.entry.set_text(text)
|
||||
self.entry.emit("activate")
|
||||
self.assertEqual(self.entry.value, value, f"text=\"{text}\"")
|
Loading…
Reference in New Issue