From 2952776fd076d47d31da3339cf603dae5ff7bb4b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 4 Apr 2021 10:27:59 -0400 Subject: [PATCH] Add table.changed event emitter (#209) * add callback on table cell change * fix tests * fix event test --- examples/table.py | 3 +++ magicgui/backends/_qtpy/widgets.py | 25 +++++++++++++++++-------- magicgui/events.py | 7 +++++-- magicgui/widgets/_protocols.py | 4 ++-- magicgui/widgets/_table.py | 16 ++++++++++++++++ tests/conftest.py | 9 +++++++++ tests/test_events.py | 2 +- 7 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 tests/conftest.py diff --git a/examples/table.py b/examples/table.py index 2f23b56c1..df4dc3154 100644 --- a/examples/table.py +++ b/examples/table.py @@ -58,4 +58,7 @@ # table.data.to_numpy() # table.to_dataframe() +# the table.changed event emits a dict of information on any cell change +# e.g. {'data': 'sdfg', 'row': 1, 'column': 0, 'column_header': '1', 'row_header': '1'} +table.changed.connect(lambda e: print(e.value)) table.show(run=True) diff --git a/magicgui/backends/_qtpy/widgets.py b/magicgui/backends/_qtpy/widgets.py index 2c4b746e9..80569db54 100644 --- a/magicgui/backends/_qtpy/widgets.py +++ b/magicgui/backends/_qtpy/widgets.py @@ -821,12 +821,21 @@ def _mgui_bind_column_headers_change_callback(self, callback) -> None: """Bind callback to column headers change event.""" raise NotImplementedError() - def _mgui_bind_cell_change_callback(self, callback) -> None: - """Bind callback to column headers change event.""" - raise NotImplementedError() - def _mgui_bind_change_callback(self, callback): - # FIXME: this currently reads out the WHOLE table every time we change ANY cell. - # nonsense. - # self._qwidget.itemChanged.connect(lambda i: callback(self._mgui_get_value())) - pass + """Bind callback to event of changing any cell.""" + + def _item_callback(item, callback=callback): + col_head = item.tableWidget().horizontalHeaderItem(item.column()) + col_head = col_head.text() if col_head is not None else "" + row_head = item.tableWidget().verticalHeaderItem(item.row()) + row_head = row_head.text() if row_head is not None else "" + data = { + "data": item.data(self._DATA_ROLE), + "row": item.row(), + "column": item.column(), + "column_header": col_head, + "row_header": row_head, + } + callback(data) + + self._qwidget.itemChanged.connect(_item_callback) diff --git a/magicgui/events.py b/magicgui/events.py index 759a0570d..ceb1b2742 100644 --- a/magicgui/events.py +++ b/magicgui/events.py @@ -661,8 +661,11 @@ def __call__(self, *args, **kwargs) -> Event: self.disconnect(cb) finally: self._emitting = False - if event._pop_source() != self.source: - raise RuntimeError("Event source-stack mismatch.") + evsource = event._pop_source() + if evsource is not self.source: + raise RuntimeError( + f"Event source-stack mismatch. ({evsource} is not {self.source}" + ) return event diff --git a/magicgui/widgets/_protocols.py b/magicgui/widgets/_protocols.py index 8c3bf72f4..2de5adcc0 100644 --- a/magicgui/widgets/_protocols.py +++ b/magicgui/widgets/_protocols.py @@ -272,8 +272,8 @@ def _mgui_bind_column_headers_change_callback( raise NotImplementedError() @abstractmethod - def _mgui_bind_cell_change_callback(self, callback: Callable[[Any], None]) -> None: - """Bind callback to column headers change event.""" + def _mgui_bind_change_callback(self, callback: Callable[[Any], Any]) -> None: + """Bind callback to value change event.""" raise NotImplementedError() diff --git a/magicgui/widgets/_table.py b/magicgui/widgets/_table.py index d7daef5cd..12523fe1a 100644 --- a/magicgui/widgets/_table.py +++ b/magicgui/widgets/_table.py @@ -23,6 +23,7 @@ from typing_extensions import Literal from magicgui.application import use_app +from magicgui.events import EventEmitter from magicgui.widgets._bases import Widget from magicgui.widgets._protocols import TableWidgetProtocol @@ -195,6 +196,14 @@ class Table(Widget, MutableMapping[TblKey, list]): to_dict(orient='dict') Return one of many different dict-like representations of table and header data. See docstring of :meth:`to_dict` for details. + + Events + ------ + changed + Emitted whenever a cell in the table changes. the `event.value` will have a + dict of information regarding the cell that changed: + {'data': x, 'row': int, 'column': int, 'column_header': str, 'row_header': str} + CURRENTLY: only emitted on changes in the GUI. not programattic changes. """ _widget: TableWidgetProtocol @@ -230,6 +239,13 @@ def __init__( "columns": columns if columns is not None else _columns, } + def _post_init(self): + super()._post_init() + self.changed = EventEmitter(source=self, type="changed") + self._widget._mgui_bind_change_callback( + lambda *x: self.changed(value=x[0] if x else None) + ) + @property def value(self) -> dict[TblKey, Collection]: """Return dict with current `data`, `index`, and `columns` of the widget.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..6e440386c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest + + +# for now, the only backend is qt, and pytest-qt's qapp provides some nice pre-post +# test cleanup that prevents some segfaults. Once we start testing multiple backends +# this will need to change. +@pytest.fixture(autouse=True, scope="session") +def always_qapp(qapp): + return qapp diff --git a/tests/test_events.py b/tests/test_events.py index 9900aaf2b..969d0492f 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -395,7 +395,7 @@ def cb(ev): try: em() except RuntimeError as err: - if str(err) != "Event source-stack mismatch.": + if "Event source-stack mismatch." not in str(err): raise em.disconnect()