From 68140475dd16cf7670c60e20d4f7be2c86fcfd67 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Mon, 24 Apr 2023 12:01:52 -0400 Subject: [PATCH 01/14] Basic proxymodel filtering --- glue/viewers/table/qt/data_viewer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 674989123..4e48513cd 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -260,6 +260,7 @@ def __init__(self, session, state=None, parent=None, widget=None): self.data = None self.model = None + self.ui.table.selection_changed.connect(self.selection_changed) self.state.add_callback('layers', self._on_layers_changed) @@ -305,8 +306,13 @@ def _on_layers_changed(self, *args): self.data = layer_state.layer self.setUpdatesEnabled(False) self.model = DataTableModel(self) - self.ui.table.setModel(self.model) + self.setUpdatesEnabled(True) + self.proxyModel = QtCore.QSortFilterProxyModel(self) + self.proxyModel.setSourceModel(self.model) + self.proxyModel.setFilterFixedString("cat") + self.proxyModel.setFilterKeyColumn(1) + self.ui.table.setModel(self.proxyModel) @messagebox_on_error("Failed to add data") def add_data(self, data): From 7a55230a4d8a864ee8d9f25e60c376f576b53594 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Mon, 24 Apr 2023 16:06:47 -0400 Subject: [PATCH 02/14] Add basic search/filter using proxyModel.setFilterFixedString --- glue/viewers/table/qt/data_viewer.py | 38 +++++++++++++++++++++++----- glue/viewers/table/qt/data_viewer.ui | 17 +++++++++++++ glue/viewers/table/state.py | 25 ++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 glue/viewers/table/state.py diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 4e48513cd..007435b43 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -7,6 +7,7 @@ from qtpy import QtCore, QtGui, QtWidgets from matplotlib.colors import ColorConverter +from echo.qt import connect_combo_selection, connect_text from glue.utils.qt import get_qapp from glue.config import viewer_tool from glue.core import BaseData, Data @@ -20,6 +21,7 @@ from glue.utils.qt import mpl_to_qt_color, messagebox_on_error from glue.core.exceptions import IncompatibleAttribute from glue.viewers.table.compat import update_table_viewer_state +from glue.viewers.table.state import TableViewerState try: import dask.array as da @@ -238,6 +240,7 @@ class TableViewer(DataViewer): _toolbar_cls = BasicToolbar _data_artist_cls = TableLayerArtist _subset_artist_cls = TableLayerArtist + _state_cls = TableViewerState inherit_tools = False tools = ['table:rowselect'] @@ -260,12 +263,27 @@ def __init__(self, session, state=None, parent=None, widget=None): self.data = None self.model = None + self.proxyModel = QtCore.QSortFilterProxyModel(self) - self.ui.table.selection_changed.connect(self.selection_changed) + self._connection1 = connect_combo_selection(self.state, 'filter_att', self.ui.combosel_filter_att) + self._connection2 = connect_text(self.state, 'filter', self.ui.valuetext_filter) + self.state.add_callback('filter', self._on_filter_changed) + self.state.add_callback('filter_att', self._on_filter_changed) + + self.ui.table.selection_changed.connect(self.selection_changed) self.state.add_callback('layers', self._on_layers_changed) self._on_layers_changed() + def get_col(self, cid): + """ + Get the column id for a ComponentID, necessary for QtFilterProxy + """ + for i, comp in enumerate(self.model.columns): + if comp == cid: + return i + return -1 # This defaults to searching all columns + def selection_changed(self): app = get_qapp() if app.queryKeyboardModifiers() == Qt.AltModifier: @@ -290,6 +308,13 @@ def finalize_selection(self, clear=True): self.ui.table.clearSelection() self.ui.table.blockSignals(False) + def _on_filter_changed(self, *args): + if (self.proxyModel is None) or (self.model is None): + return + self.proxyModel.invalidateFilter() + self.proxyModel.setFilterFixedString(self.state.filter) + self.proxyModel.setFilterKeyColumn(self.get_col(self.state.filter_att)) + def _on_layers_changed(self, *args): for layer_state in self.state.layers: if isinstance(layer_state.layer, BaseData): @@ -304,15 +329,16 @@ def _on_layers_changed(self, *args): return self.data = layer_state.layer + self.state.filter_att_helper.set_multiple_data([self.data]) + self.setUpdatesEnabled(False) self.model = DataTableModel(self) - - self.setUpdatesEnabled(True) - self.proxyModel = QtCore.QSortFilterProxyModel(self) + self.proxyModel.invalidateFilter() self.proxyModel.setSourceModel(self.model) - self.proxyModel.setFilterFixedString("cat") - self.proxyModel.setFilterKeyColumn(1) + self.proxyModel.setFilterFixedString(self.state.filter) + self.proxyModel.setFilterKeyColumn(self.get_col(self.state.filter_att)) self.ui.table.setModel(self.proxyModel) + self.setUpdatesEnabled(True) @messagebox_on_error("Failed to add data") def add_data(self, data): diff --git a/glue/viewers/table/qt/data_viewer.ui b/glue/viewers/table/qt/data_viewer.ui index 499fe6285..7a5ce0ff1 100644 --- a/glue/viewers/table/qt/data_viewer.ui +++ b/glue/viewers/table/qt/data_viewer.ui @@ -15,6 +15,23 @@ + + + + + Filter/Search + + + + + + + + + + + + diff --git a/glue/viewers/table/state.py b/glue/viewers/table/state.py new file mode 100644 index 000000000..5d502e3a6 --- /dev/null +++ b/glue/viewers/table/state.py @@ -0,0 +1,25 @@ +from echo import CallbackProperty, SelectionCallbackProperty +from glue.core.data_combo_helper import ComponentIDComboHelper + +from glue.viewers.common.state import ViewerState + + +__all__ = ['TableViewerState'] + + +class TableViewerState(ViewerState): + """ + A state class that includes all the attributes for a table viewer. + """ + + filter_att = SelectionCallbackProperty(docstring='The component/column to filter/search on') + filter = CallbackProperty(docstring='The text string to filter/search on') + + def __init__(self, **kwargs): + + super(TableViewerState, self).__init__() + + self.filter_att_helper = ComponentIDComboHelper(self, 'filter_att', none=True, categorical=True, numeric=False) + + self.update_from_dict(kwargs) + From 8f12602df783902a8fc84d940cfc89032f97e18a Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Tue, 25 Apr 2023 10:36:42 -0400 Subject: [PATCH 03/14] Require column for filtering --- glue/viewers/table/state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glue/viewers/table/state.py b/glue/viewers/table/state.py index 5d502e3a6..7a9be16ae 100644 --- a/glue/viewers/table/state.py +++ b/glue/viewers/table/state.py @@ -12,14 +12,14 @@ class TableViewerState(ViewerState): A state class that includes all the attributes for a table viewer. """ - filter_att = SelectionCallbackProperty(docstring='The component/column to filter/search on') + filter_att = SelectionCallbackProperty(docstring='The component/column to filter/search on', default_index=0) filter = CallbackProperty(docstring='The text string to filter/search on') def __init__(self, **kwargs): super(TableViewerState, self).__init__() - self.filter_att_helper = ComponentIDComboHelper(self, 'filter_att', none=True, categorical=True, numeric=False) + self.filter_att_helper = ComponentIDComboHelper(self, 'filter_att', categorical=True, numeric=False) self.update_from_dict(kwargs) From f8442c6528b986c2033560ec8ae0ed71e9eeaf1d Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Tue, 25 Apr 2023 10:41:44 -0400 Subject: [PATCH 04/14] Improve layout of search/filter field --- glue/viewers/table/qt/data_viewer.ui | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/glue/viewers/table/qt/data_viewer.ui b/glue/viewers/table/qt/data_viewer.ui index 7a5ce0ff1..4234aae32 100644 --- a/glue/viewers/table/qt/data_viewer.ui +++ b/glue/viewers/table/qt/data_viewer.ui @@ -16,16 +16,36 @@ + + + + Qt::Horizontal + + + + 60 + 20 + + + + - Filter/Search + Search: + + + + in + + + From 214c2807d3ffce7b0ae0148d2ecb871add6cea17 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Tue, 25 Apr 2023 11:43:07 -0400 Subject: [PATCH 05/14] Rename proxy filter --- glue/viewers/table/qt/data_viewer.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 007435b43..f224a4610 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -263,7 +263,7 @@ def __init__(self, session, state=None, parent=None, widget=None): self.data = None self.model = None - self.proxyModel = QtCore.QSortFilterProxyModel(self) + self.proxy_model = QtCore.QSortFilterProxyModel(self) self._connection1 = connect_combo_selection(self.state, 'filter_att', self.ui.combosel_filter_att) self._connection2 = connect_text(self.state, 'filter', self.ui.valuetext_filter) @@ -309,11 +309,11 @@ def finalize_selection(self, clear=True): self.ui.table.blockSignals(False) def _on_filter_changed(self, *args): - if (self.proxyModel is None) or (self.model is None): + if (self.proxy_model is None) or (self.model is None): return - self.proxyModel.invalidateFilter() - self.proxyModel.setFilterFixedString(self.state.filter) - self.proxyModel.setFilterKeyColumn(self.get_col(self.state.filter_att)) + self.proxy_model.invalidateFilter() + self.proxy_model.setFilterFixedString(self.state.filter) + self.proxy_model.setFilterKeyColumn(self.get_col(self.state.filter_att)) def _on_layers_changed(self, *args): for layer_state in self.state.layers: @@ -333,11 +333,11 @@ def _on_layers_changed(self, *args): self.setUpdatesEnabled(False) self.model = DataTableModel(self) - self.proxyModel.invalidateFilter() - self.proxyModel.setSourceModel(self.model) - self.proxyModel.setFilterFixedString(self.state.filter) - self.proxyModel.setFilterKeyColumn(self.get_col(self.state.filter_att)) - self.ui.table.setModel(self.proxyModel) + self.proxy_model.invalidateFilter() + self.proxy_model.setSourceModel(self.model) + self.proxy_model.setFilterFixedString(self.state.filter) + self.proxy_model.setFilterKeyColumn(self.get_col(self.state.filter_att)) + self.ui.table.setModel(self.proxy_model) self.setUpdatesEnabled(True) @messagebox_on_error("Failed to add data") From 275310171207b6449732a9c6fc7c2c2fefd34d7a Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Wed, 26 Apr 2023 14:23:11 -0400 Subject: [PATCH 06/14] Do filtering by hand --- glue/viewers/table/qt/data_viewer.py | 56 +++++++++++++++++++--------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index f224a4610..6afe2df81 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -1,5 +1,6 @@ import os from functools import lru_cache +import re import numpy as np @@ -42,6 +43,8 @@ def __init__(self, table_viewer): raise ValueError("Can only use Table widget for 1D data") self._table_viewer = table_viewer self._data = table_viewer.data + self._state = table_viewer.state + self.filter_mask = None self.show_coords = False self.order = np.arange(self._data.shape[0]) self._update_visible() @@ -151,9 +154,17 @@ def sort(self, column, ascending): self.data_by_row_and_column.cache_clear() self.layoutChanged.emit() + def get_filter_mask(self): + p = re.compile(self._state.filter) + comp = self._data.get_component(self._state.filter_att) + self.filter_mask = np.array([bool(p.search(x)) for x in comp.data]) + self.data_changed() # This might be overkill + + def _update_visible(self): """ - Given which layers are visible or not, convert order to order_visible. + Given which layers are visible or not, convert order to order_visible + after applying the current filter_mask """ self.data_by_row_and_column.cache_clear() @@ -161,11 +172,19 @@ def _update_visible(self): # First, if the data layer is visible, show all rows for layer_artist in self._table_viewer.layers: if layer_artist.visible and isinstance(layer_artist.layer, BaseData): - self.order_visible = self.order - return + if self.filter_mask is None: + self.order_visible = self.order + return + else: + mask = self.filter_mask[self.order] + self.order_visible = self.order[mask] + return # If not then we need to show only the rows with visible subsets - visible = np.zeros(self.order.shape, dtype=bool) + if self.filter_mask is None: + visible = np.zeros(self.order.shape, dtype=bool) + else: + visible = self.filter_mask[self.order] for layer_artist in self._table_viewer.layers: if layer_artist.visible: mask = layer_artist.layer.to_mask()[self.order] @@ -216,7 +235,7 @@ def __init__(self, viewer): def activate(self): self.viewer.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - def deactivate(self): + def deactivate(self, block=False): # Don't do anything if the viewer has already been closed if self.viewer is None: return @@ -225,7 +244,9 @@ def deactivate(self): class TableViewWithSelectionSignal(QtWidgets.QTableView): - + """ + This is the TableViewer.ui.table object + """ selection_changed = QtCore.Signal() def selectionChanged(self, *args, **kwargs): @@ -263,8 +284,6 @@ def __init__(self, session, state=None, parent=None, widget=None): self.data = None self.model = None - self.proxy_model = QtCore.QSortFilterProxyModel(self) - self._connection1 = connect_combo_selection(self.state, 'filter_att', self.ui.combosel_filter_att) self._connection2 = connect_text(self.state, 'filter', self.ui.valuetext_filter) @@ -309,11 +328,16 @@ def finalize_selection(self, clear=True): self.ui.table.blockSignals(False) def _on_filter_changed(self, *args): - if (self.proxy_model is None) or (self.model is None): - return - self.proxy_model.invalidateFilter() - self.proxy_model.setFilterFixedString(self.state.filter) - self.proxy_model.setFilterKeyColumn(self.get_col(self.state.filter_att)) + # If we change the filter we deactivate the toolbar to keep + # any subset defined before we change what is displayed + if self.toolbar.active_tool is self.toolbar.tools['table:rowselect']: + old_tool = self.toolbar.active_tool + old_tool.deactivate(block=True) + button = self.toolbar.actions[old_tool.tool_id] + if button.isChecked(): + button.setChecked(False) + if self.model: + self.model.get_filter_mask() def _on_layers_changed(self, *args): for layer_state in self.state.layers: @@ -333,11 +357,7 @@ def _on_layers_changed(self, *args): self.setUpdatesEnabled(False) self.model = DataTableModel(self) - self.proxy_model.invalidateFilter() - self.proxy_model.setSourceModel(self.model) - self.proxy_model.setFilterFixedString(self.state.filter) - self.proxy_model.setFilterKeyColumn(self.get_col(self.state.filter_att)) - self.ui.table.setModel(self.proxy_model) + self.ui.table.setModel(self.model) self.setUpdatesEnabled(True) @messagebox_on_error("Failed to add data") From b3fe89f8c80a2d7991711ef891cab85116ae239b Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Wed, 26 Apr 2023 15:19:22 -0400 Subject: [PATCH 07/14] Minor formatting --- glue/viewers/table/qt/data_viewer.py | 3 +-- glue/viewers/table/state.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 6afe2df81..26a3461ae 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -158,8 +158,7 @@ def get_filter_mask(self): p = re.compile(self._state.filter) comp = self._data.get_component(self._state.filter_att) self.filter_mask = np.array([bool(p.search(x)) for x in comp.data]) - self.data_changed() # This might be overkill - + self.data_changed() # This might be overkill def _update_visible(self): """ diff --git a/glue/viewers/table/state.py b/glue/viewers/table/state.py index 7a9be16ae..659bb1e3b 100644 --- a/glue/viewers/table/state.py +++ b/glue/viewers/table/state.py @@ -22,4 +22,3 @@ def __init__(self, **kwargs): self.filter_att_helper = ComponentIDComboHelper(self, 'filter_att', categorical=True, numeric=False) self.update_from_dict(kwargs) - From ed7f1bbdc81da74b552aa82c6fd90a4a01fb1c85 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Wed, 26 Apr 2023 15:32:21 -0400 Subject: [PATCH 08/14] Remove old function --- glue/viewers/table/qt/data_viewer.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 26a3461ae..6d268dcb0 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -293,15 +293,6 @@ def __init__(self, session, state=None, parent=None, widget=None): self.state.add_callback('layers', self._on_layers_changed) self._on_layers_changed() - def get_col(self, cid): - """ - Get the column id for a ComponentID, necessary for QtFilterProxy - """ - for i, comp in enumerate(self.model.columns): - if comp == cid: - return i - return -1 # This defaults to searching all columns - def selection_changed(self): app = get_qapp() if app.queryKeyboardModifiers() == Qt.AltModifier: From e334cf1f906278a7962b31799da9a71a3741e39f Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Wed, 26 Apr 2023 17:01:18 -0400 Subject: [PATCH 09/14] Make save/restore work with new filtering attributes --- glue/viewers/table/compat.py | 5 ++++- glue/viewers/table/qt/data_viewer.py | 1 - glue/viewers/table/state.py | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/glue/viewers/table/compat.py b/glue/viewers/table/compat.py index 75593a86f..01155a658 100644 --- a/glue/viewers/table/compat.py +++ b/glue/viewers/table/compat.py @@ -22,7 +22,10 @@ def update_table_viewer_state(rec, context): rec['state'] = {} rec['state']['values'] = {} - rec.pop('properties') + properties = rec.pop('properties') + viewer_state = rec['state']['values'] + viewer_state['filter_att'] = properties['filter_att'] + viewer_state['filter'] = properties['filter'] layer_states = [] diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 6d268dcb0..be2716674 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -343,7 +343,6 @@ def _on_layers_changed(self, *args): return self.data = layer_state.layer - self.state.filter_att_helper.set_multiple_data([self.data]) self.setUpdatesEnabled(False) self.model = DataTableModel(self) diff --git a/glue/viewers/table/state.py b/glue/viewers/table/state.py index 659bb1e3b..31b7b2862 100644 --- a/glue/viewers/table/state.py +++ b/glue/viewers/table/state.py @@ -20,5 +20,25 @@ def __init__(self, **kwargs): super(TableViewerState, self).__init__() self.filter_att_helper = ComponentIDComboHelper(self, 'filter_att', categorical=True, numeric=False) + self.add_callback('layers', self._layers_changed) self.update_from_dict(kwargs) + + def _layers_changed(self, *args): + + layers_data = self.layers_data + + layers_data_cache = getattr(self, '_layers_data_cache', []) + + if layers_data == layers_data_cache: + return + + self.filter_att_helper.set_multiple_data(self.layers_data) + + self._layers_data_cache = layers_data + + def _update_priority(self, name): + if name == 'layers': + return 2 + else: + return 1 From bb63d2f91bf9c623ad865a588339a170dc668a37 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Thu, 27 Apr 2023 09:19:02 -0400 Subject: [PATCH 10/14] Remove problematic addition to compatability shim --- glue/viewers/table/compat.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/glue/viewers/table/compat.py b/glue/viewers/table/compat.py index 01155a658..398e49841 100644 --- a/glue/viewers/table/compat.py +++ b/glue/viewers/table/compat.py @@ -23,9 +23,6 @@ def update_table_viewer_state(rec, context): rec['state']['values'] = {} properties = rec.pop('properties') - viewer_state = rec['state']['values'] - viewer_state['filter_att'] = properties['filter_att'] - viewer_state['filter'] = properties['filter'] layer_states = [] From 089e87ca650d42bca6836264c4402c978b6ecd37 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Thu, 27 Apr 2023 09:56:17 -0400 Subject: [PATCH 11/14] Add tests for filtering --- glue/viewers/table/qt/data_viewer.py | 5 ++ .../table/qt/tests/test_data_viewer.py | 85 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index be2716674..b0c07c1ae 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -155,6 +155,9 @@ def sort(self, column, ascending): self.layoutChanged.emit() def get_filter_mask(self): + if (self._state.filter is None) or (self._state.filter_att is None): + self.filter_mask = np.ones(self.order.shape, dtype=bool) + return p = re.compile(self._state.filter) comp = self._data.get_component(self._state.filter_att) self.filter_mask = np.array([bool(p.search(x)) for x in comp.data]) @@ -346,6 +349,8 @@ def _on_layers_changed(self, *args): self.setUpdatesEnabled(False) self.model = DataTableModel(self) + self.model.get_filter_mask() + self.ui.table.setModel(self.model) self.setUpdatesEnabled(True) diff --git a/glue/viewers/table/qt/tests/test_data_viewer.py b/glue/viewers/table/qt/tests/test_data_viewer.py index 07314fac7..41fd8fb48 100644 --- a/glue/viewers/table/qt/tests/test_data_viewer.py +++ b/glue/viewers/table/qt/tests/test_data_viewer.py @@ -611,3 +611,88 @@ def press_key(key): colors[1] = color check_values_and_color(post_model, data, colors) + + +def test_table_widget_filter(tmpdir): + + # Test table interactions with filtering + + app = get_qapp() # noqa + + d = Data(a=[1, 2, 3, 4, 5], + b=['cat', 'dog', 'cat', 'dog', 'fish'], + c=['fluffy', 'rover', 'fluffball', 'spot', 'moby'], label='test') + + dc = DataCollection([d]) + + gapp = GlueApplication(dc) + + widget = gapp.new_data_viewer(TableViewer) + widget.add_data(d) + + widget.state.filter_att = d.components[2] + + widget.state.filter = 'cat' + model = widget.ui.table.model() + + np.testing.assert_equal(model.filter_mask, [True, False, True, False, False]) + + widget.state.filter_att = d.components[3] + widget.state.filter = 'ff' + np.testing.assert_equal(model.filter_mask, [True, False, True, False, False]) + + # Test matching regular expressions + widget.state.filter_att = d.components[3] + widget.state.filter = '^[a-z]{1}o' + np.testing.assert_equal(model.filter_mask, [False, True, False, False, True]) + + sg1 = dc.new_subset_group('test subset 1', d.id['a'] > 2) + sg1.style.color = '#aa0000' + data = {'a': [2, 5], + 'b': ['dog', 'fish'], + 'c': ['rover', 'moby']} + + colors = [None, '#aa0000'] + + check_values_and_color(model, data, colors) + + +def test_table_widget_session_filter(tmpdir): + + # Test that filtering works with save/restore + + app = get_qapp() # noqa + + d = Data(a=[1, 2, 3, 4, 5], + b=['cat', 'dog', 'cat', 'dog', 'fish'], + c=['fluffy', 'rover', 'fluffball', 'spot', 'moby'], label='test') + + dc = DataCollection([d]) + + gapp = GlueApplication(dc) + + widget = gapp.new_data_viewer(TableViewer) + widget.add_data(d) + + widget.state.filter_att = d.components[2] + widget.state.filter = 'cat' + model = widget.ui.table.model() + + np.testing.assert_equal(model.filter_mask, [True, False, True, False, False]) + + session_file = tmpdir.join('table.glu').strpath + + gapp.save_session(session_file) + + gapp2 = GlueApplication.restore_session(session_file) + gapp2.show() + + d = gapp2.data_collection[0] + + widget2 = gapp2.viewers[0][0] + + model2 = widget2.ui.table.model() + + assert widget2.state.filter_att == d.components[2] + assert widget2.state.filter == 'cat' + np.testing.assert_equal(model2.filter_mask, [True, False, True, False, False]) From 324df1f0d3be83732bec6f19a4a1ebe9a133b734 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Thu, 27 Apr 2023 12:17:10 -0400 Subject: [PATCH 12/14] Add button+state to enable/disable using regex to interpret filter string --- glue/viewers/table/qt/data_viewer.py | 12 +++++++++--- glue/viewers/table/qt/data_viewer.ui | 7 +++++++ glue/viewers/table/qt/tests/test_data_viewer.py | 12 ++++++++++++ glue/viewers/table/state.py | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index b0c07c1ae..05903f920 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -8,7 +8,7 @@ from qtpy import QtCore, QtGui, QtWidgets from matplotlib.colors import ColorConverter -from echo.qt import connect_combo_selection, connect_text +from echo.qt import connect_combo_selection, connect_text, connect_checkable_button from glue.utils.qt import get_qapp from glue.config import viewer_tool from glue.core import BaseData, Data @@ -158,9 +158,13 @@ def get_filter_mask(self): if (self._state.filter is None) or (self._state.filter_att is None): self.filter_mask = np.ones(self.order.shape, dtype=bool) return - p = re.compile(self._state.filter) comp = self._data.get_component(self._state.filter_att) - self.filter_mask = np.array([bool(p.search(x)) for x in comp.data]) + + if self._state.regex: + p = re.compile(self._state.filter) + self.filter_mask = np.array([bool(p.search(x)) for x in comp.data]) + else: + self.filter_mask = np.array([self._state.filter in x for x in comp.data]) self.data_changed() # This might be overkill def _update_visible(self): @@ -288,7 +292,9 @@ def __init__(self, session, state=None, parent=None, widget=None): self._connection1 = connect_combo_selection(self.state, 'filter_att', self.ui.combosel_filter_att) self._connection2 = connect_text(self.state, 'filter', self.ui.valuetext_filter) + self._connection3 = connect_checkable_button(self.state, 'regex', self.ui.bool_regex) + self.state.add_callback('regex', self._on_filter_changed) self.state.add_callback('filter', self._on_filter_changed) self.state.add_callback('filter_att', self._on_filter_changed) diff --git a/glue/viewers/table/qt/data_viewer.ui b/glue/viewers/table/qt/data_viewer.ui index 4234aae32..a79e5054d 100644 --- a/glue/viewers/table/qt/data_viewer.ui +++ b/glue/viewers/table/qt/data_viewer.ui @@ -49,6 +49,13 @@ + + + + Use regex + + + diff --git a/glue/viewers/table/qt/tests/test_data_viewer.py b/glue/viewers/table/qt/tests/test_data_viewer.py index 41fd8fb48..35fc7a3c4 100644 --- a/glue/viewers/table/qt/tests/test_data_viewer.py +++ b/glue/viewers/table/qt/tests/test_data_viewer.py @@ -643,6 +643,8 @@ def test_table_widget_filter(tmpdir): # Test matching regular expressions widget.state.filter_att = d.components[3] + widget.state.regex = True + widget.state.filter = '^[a-z]{1}o' np.testing.assert_equal(model.filter_mask, [False, True, False, False, True]) @@ -656,6 +658,16 @@ def test_table_widget_filter(tmpdir): check_values_and_color(model, data, colors) + widget.state.regex = False + widget.state.filter = '^[a-z]{1}o' + np.testing.assert_equal(model.filter_mask, [False, False, False, False, False]) + + # Check that changing the filter disables the rowselect tool + widget.toolbar.actions['table:rowselect'].toggle() + process_events() + widget.state.filter_att = d.components[2] + assert widget.toolbar.active_tool is None + def test_table_widget_session_filter(tmpdir): diff --git a/glue/viewers/table/state.py b/glue/viewers/table/state.py index 31b7b2862..cefbece0e 100644 --- a/glue/viewers/table/state.py +++ b/glue/viewers/table/state.py @@ -14,11 +14,11 @@ class TableViewerState(ViewerState): filter_att = SelectionCallbackProperty(docstring='The component/column to filter/search on', default_index=0) filter = CallbackProperty(docstring='The text string to filter/search on') + regex = CallbackProperty(docstring='Whether to apply regex to filter/search', default=False) def __init__(self, **kwargs): super(TableViewerState, self).__init__() - self.filter_att_helper = ComponentIDComboHelper(self, 'filter_att', categorical=True, numeric=False) self.add_callback('layers', self._layers_changed) From 1fc4686997124224e326302f0044e0996a14e62e Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Fri, 12 May 2023 09:03:01 -0400 Subject: [PATCH 13/14] Reflect that we don't use properties value --- glue/viewers/table/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/viewers/table/compat.py b/glue/viewers/table/compat.py index 398e49841..594cf103e 100644 --- a/glue/viewers/table/compat.py +++ b/glue/viewers/table/compat.py @@ -22,7 +22,7 @@ def update_table_viewer_state(rec, context): rec['state'] = {} rec['state']['values'] = {} - properties = rec.pop('properties') + _ = rec.pop('properties') layer_states = [] From e7f55d7375aae12dad8f086d2c9cec99d4ce25e4 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Fri, 12 May 2023 09:30:47 -0400 Subject: [PATCH 14/14] Use autoconnect_callbacks --- glue/viewers/table/qt/data_viewer.py | 6 ++---- glue/viewers/table/qt/data_viewer.ui | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 05903f920..3d7e7220e 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -8,7 +8,7 @@ from qtpy import QtCore, QtGui, QtWidgets from matplotlib.colors import ColorConverter -from echo.qt import connect_combo_selection, connect_text, connect_checkable_button +from echo.qt import autoconnect_callbacks_to_qt from glue.utils.qt import get_qapp from glue.config import viewer_tool from glue.core import BaseData, Data @@ -290,9 +290,7 @@ def __init__(self, session, state=None, parent=None, widget=None): self.data = None self.model = None - self._connection1 = connect_combo_selection(self.state, 'filter_att', self.ui.combosel_filter_att) - self._connection2 = connect_text(self.state, 'filter', self.ui.valuetext_filter) - self._connection3 = connect_checkable_button(self.state, 'regex', self.ui.bool_regex) + self._connections = autoconnect_callbacks_to_qt(self.state, self.ui) self.state.add_callback('regex', self._on_filter_changed) self.state.add_callback('filter', self._on_filter_changed) diff --git a/glue/viewers/table/qt/data_viewer.ui b/glue/viewers/table/qt/data_viewer.ui index a79e5054d..629809f2c 100644 --- a/glue/viewers/table/qt/data_viewer.ui +++ b/glue/viewers/table/qt/data_viewer.ui @@ -37,7 +37,7 @@ - +