From 2b5cdaa197e38b17b788f861816116008ba7412e Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Sun, 4 Jun 2023 08:51:56 -0400 Subject: [PATCH] action: Add an ActionEntry class This is inspried by the Gio.ActionEntry struct, which I can't figure out how to get working in Python. I add on a few extra helpful features, such as: - Automatically creating a Gio.SimpleAction - Tracking the desired accelerator keys - Binding the "enabled" state to a specificed property at construction Signed-off-by: Anna Schumaker --- emmental/action.py | 39 ++++++++++++++++++++++++++ tests/test_action.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 emmental/action.py create mode 100644 tests/test_action.py diff --git a/emmental/action.py b/emmental/action.py new file mode 100644 index 0000000..d5ecffc --- /dev/null +++ b/emmental/action.py @@ -0,0 +1,39 @@ +# Copyright 2023 (c) Anna Schumaker. +"""A custom ActionEntry that works in Python.""" +from gi.repository import GObject +from gi.repository import Gio +from gi.repository import Gtk + + +class ActionEntry(GObject.GObject): + """Our own AcitionEntry class to make accelerators easier.""" + + enabled = GObject.Property(type=bool, default=True) + + def __init__(self, name: str, func: callable, *accels: tuple[str], + enabled: tuple[GObject.GObject, str] | None = None): + """Initialize an ActionEntry.""" + super().__init__() + + for accel in accels: + if not Gtk.accelerator_parse(accel)[0]: + raise ValueError + + self.accels = list(accels) + self.func = func + + if enabled is not None: + self.enabled = enabled[0].get_property(enabled[1]) + enabled[0].bind_property(enabled[1], self, "enabled") + + self.action = Gio.SimpleAction(name=name, enabled=self.enabled) + self.action.connect("activate", self.__activate) + self.bind_property("enabled", self.action, "enabled") + + def __activate(self, action: Gio.SimpleAction, param) -> None: + self.func() + + @property + def name(self) -> str: + """Get then name of this ActionEntry.""" + return self.action.get_name() diff --git a/tests/test_action.py b/tests/test_action.py new file mode 100644 index 0000000..b5a6d33 --- /dev/null +++ b/tests/test_action.py @@ -0,0 +1,66 @@ +# Copyright 2023 (c) Anna Schumaker. +"""Tests our Python ActionEntry class.""" +import unittest +import emmental.action +from gi.repository import GObject +from gi.repository import Gio +from gi.repository import Gtk + + +class TestActionEntry(unittest.TestCase): + """Test case for our Python ActionEntry.""" + + def test_init(self): + """Test constructing an ActionEntry.""" + func = unittest.mock.Mock() + entry = emmental.action.ActionEntry("test-name", func, "t") + self.assertIsInstance(entry, GObject.GObject) + self.assertIsInstance(entry.action, Gio.SimpleAction) + self.assertEqual(entry.action.get_name(), "test-name") + + self.assertEqual(entry.name, "test-name") + self.assertEqual(entry.func, func) + self.assertListEqual(entry.accels, ["t"]) + + def test_multiple_accels(self): + """Test that multiple accelerators can be passed.""" + func = unittest.mock.Mock() + entry = emmental.action.ActionEntry("test-multi", func, + "t", "u") + self.assertListEqual(entry.accels, ["t", "u"]) + + def test_invalid_accel(self): + """Test that invalid accelerators are caught during construction.""" + func = unittest.mock.Mock() + with self.assertRaises(ValueError): + emmental.action.ActionEntry("test-name", func, "") + + def test_activate(self): + """Test activating the constructed action.""" + func = unittest.mock.Mock() + entry = emmental.action.ActionEntry("test-name", func, "t") + entry.action.activate() + func.assert_called_with() + + def test_enabled(self): + """Test the enabled property.""" + func = unittest.mock.Mock() + entry = emmental.action.ActionEntry("test-name", func, "t") + self.assertTrue(entry.enabled) + self.assertTrue(entry.action.get_enabled()) + + entry.enabled = False + self.assertFalse(entry.action.get_enabled()) + + def test_enabled_bind(self): + """Test binding to the enabled property at construction.""" + func = unittest.mock.Mock() + label = Gtk.Label(sensitive=False) + entry = emmental.action.ActionEntry("test-name", func, "t", + enabled=(label, "sensitive")) + self.assertFalse(entry.enabled) + self.assertFalse(entry.action.get_enabled()) + + label.set_sensitive(True) + self.assertTrue(entry.enabled) + self.assertTrue(entry.action.get_enabled())