Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add table.changed event emitter #209

Merged
merged 3 commits into from
Apr 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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