Skip to content

Commit

Permalink
Add table.changed event emitter (#209)
Browse files Browse the repository at this point in the history
* add callback on table cell change

* fix tests

* fix event test
  • Loading branch information
tlambert03 committed Apr 4, 2021
1 parent f0bfa8f commit 2952776
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 13 deletions.
3 changes: 3 additions & 0 deletions examples/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
25 changes: 17 additions & 8 deletions magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
7 changes: 5 additions & 2 deletions magicgui/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions magicgui/widgets/_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
16 changes: 16 additions & 0 deletions magicgui/widgets/_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 2952776

Please sign in to comment.