factory: Create ListRow and Factory classes

The ListRow class is intended to be used as a base class for displaying
individual Gtk.ListView rows. The implement some helpful functionality
to make it easier to bind list items to child widgets.

The Factory class is designed to create ListRow widgets.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-09-26 14:20:51 -04:00
parent 5d3fb980af
commit 711fa0da5b
2 changed files with 213 additions and 0 deletions

99
emmental/factory.py Normal file
View File

@ -0,0 +1,99 @@
# Copyright 2022 (c) Anna Schumaker.
"""A customized Gtk.SignalListItemFactory for easier use."""
import typing
from gi.repository import GObject
from gi.repository import Gtk
class ListRow(GObject.GObject):
"""Extra state that we attach to the Gtk.ListItem."""
listitem = GObject.Property(type=Gtk.ListItem)
def __init__(self, listitem: Gtk.ListItem, **kwargs):
"""Initialize a ListRow object."""
GObject.GObject.__init__(self, listitem=listitem, **kwargs)
self.bindings = []
def do_bind(self) -> None:
"""Bind the list item to the child widget."""
def do_unbind(self) -> None:
"""Unbind the list item from the child widget."""
def bind_and_set(self, src: GObject.GObject, src_prop: str,
dst: GObject.GObject, dst_prop: str,
bidirectional: bool = False,
invert_boolean: bool = False) -> None:
"""Bind and set a property from the src object to the dst object."""
f_bidir = GObject.BindingFlags.BIDIRECTIONAL if bidirectional else 0
f_invrt = GObject.BindingFlags.INVERT_BOOLEAN if invert_boolean else 0
src_value = src.get_property(src_prop)
value = not src_value if invert_boolean else src_value
dst.set_property(dst_prop, value)
self.bindings.append(src.bind_property(src_prop, dst, dst_prop,
f_bidir | f_invrt))
def bind_and_set_property(self, item_prop: str, child_prop: str,
bidirectional: bool = False,
invert_boolean: bool = False) -> None:
"""Bind and set a list item property."""
self.bind_and_set(self.item, item_prop, self.child, child_prop,
bidirectional, invert_boolean)
def bind(self) -> None:
"""Bind the list item to the child widget."""
self.do_bind()
def unbind(self) -> None:
"""Unbind the list item from the child widget."""
for binding in self.bindings:
binding.unbind()
self.bindings.clear()
self.do_unbind()
@GObject.Property(type=Gtk.Widget)
def child(self) -> Gtk.Widget | None:
"""Get the child widget displayed by this Row."""
return self.listitem.get_child()
@child.setter
def child(self, newval: Gtk.Widget) -> None:
self.listitem.set_child(newval)
@GObject.Property(type=GObject.TYPE_PYOBJECT)
def item(self) -> GObject.TYPE_PYOBJECT:
"""Get the list item for this Row."""
return self.listitem.get_item()
class Factory(Gtk.SignalListItemFactory):
"""A customized Factory for making list row widgets."""
def __init__(self, row_type: typing.Type[ListRow], **kwargs):
"""Initialize a ListFactory."""
super().__init__()
self.row_type = row_type
self.connect("setup", self.__setup, kwargs)
self.connect("bind", self.__bind)
self.connect("unbind", self.__unbind)
self.connect("teardown", self.__teardown)
def __setup(self, factory: Gtk.SignalListItemFactory,
listitem: Gtk.ListItem, kwargs: dict) -> None:
listitem.listrow = self.row_type(listitem, **kwargs)
def __bind(self, factory: Gtk.SignalListItemFactory,
listitem: Gtk.ListItem) -> None:
listitem.listrow.bind()
def __unbind(self, factory: Gtk.SignalListItemFactory,
listitem: Gtk.ListItem) -> None:
listitem.listrow.unbind()
def __teardown(self, factory: Gtk.SignalListItemFactory,
listitem: Gtk.ListItem) -> None:
listitem.set_child(None)
listitem.listrow = None

114
tests/test_factory.py Normal file
View File

@ -0,0 +1,114 @@
# Copyright 2022 (c) Anna Schumaker.
"""Tests our Factory implementation."""
import unittest
import unittest.mock
import emmental.factory
from gi.repository import Gtk
from gi.repository import GObject
class TestListRow(unittest.TestCase):
"""Test the ListRow object."""
def setUp(self):
"""Set up common variables."""
self.item = Gtk.Label(label="Test")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.item)
self.row = emmental.factory.ListRow(self.listitem, child=Gtk.Label())
self.row.do_bind = unittest.mock.Mock(wraps=self.row.do_bind)
self.row.do_unbind = unittest.mock.Mock(wraps=self.row.do_unbind)
def test_init(self):
"""Test that the ListRow is set up properly."""
self.assertIsInstance(self.row, GObject.GObject)
self.assertIsInstance(self.listitem.get_child(), Gtk.Label)
self.assertListEqual(self.row.bindings, [])
self.assertEqual(self.row.listitem, self.listitem)
self.assertEqual(self.row.child, self.listitem.get_child())
self.assertEqual(self.row.item, self.item)
def test_bind(self):
"""Test calling bind() on the ListRow."""
self.row.bind()
self.row.do_bind.assert_called()
def test_bind_and_set_property(self):
"""Test the ListRow bind_property() function."""
self.row.bind_and_set_property("label", "label")
self.assertEqual(self.row.child.get_text(), "Test")
self.assertEqual(len(self.row.bindings), 1)
self.assertIsInstance(self.row.bindings[0], GObject.Binding)
self.item.set_text("Text 2")
self.assertEqual(self.row.child.get_text(), "Text 2")
self.row.child.set_text("Other Text")
self.assertEqual(self.item.get_text(), "Text 2")
def test_bind_bidirectional(self):
"""Test bidirectional bindings."""
self.row.bind_and_set_property("label", "label", bidirectional=True)
self.assertEqual(self.row.child.get_text(), "Test")
self.row.child.set_text("Other Text")
self.assertEqual(self.item.get_text(), "Other Text")
def test_bind_invert_boolean(self):
"""Test invert boolean bindings."""
self.row.bind_and_set_property("sensitive", "sensitive",
invert_boolean=True)
self.assertFalse(self.row.child.get_sensitive())
self.item.set_sensitive(False)
self.assertTrue(self.row.child.get_sensitive())
def test_unbind(self):
"""Test unbinding a ListRow."""
self.row.unbind()
self.row.do_unbind.assert_called()
self.assertEqual(len(self.row.bindings), 0)
class TestFactory(unittest.TestCase):
"""Test a Factory."""
def setUp(self):
"""Set up common variables."""
self.item = Gtk.Label(label="Text")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.item)
self.factory = emmental.factory.Factory(
row_type=emmental.factory.ListRow)
def test_init(self):
"""Test that the ListFactory is set up properly."""
self.assertIsInstance(self.factory, Gtk.SignalListItemFactory)
self.assertEqual(self.factory.row_type, emmental.factory.ListRow)
def test_setup(self):
"""Test the setup signal."""
self.factory.emit("setup", self.listitem)
self.assertIsInstance(self.listitem.listrow,
emmental.factory.ListRow)
def test_bind(self):
"""Test the bind signal."""
self.factory.emit("setup", self.listitem)
self.listitem.listrow.bind = unittest.mock.Mock()
self.factory.emit("bind", self.listitem)
self.listitem.listrow.bind.assert_called()
def test_unbind(self):
"""Test the unbind signal."""
self.factory.emit("setup", self.listitem)
self.listitem.listrow.unbind = unittest.mock.Mock()
self.factory.emit("unbind", self.listitem)
self.listitem.listrow.unbind.assert_called()
def test_teardown(self):
"""Test the teardown signal."""
self.factory.emit("setup", self.listitem)
self.factory.emit("teardown", self.listitem)
self.assertIsNone(self.listitem.get_child())
self.assertIsNone(self.listitem.listrow)