From 7e65241eb608b6dfbf5402695ba9989b82a950ad Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 8 Aug 2016 11:28:09 +0100 Subject: [PATCH 01/22] Enable table viewer by default --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7657a04dc..bd33d584a 100755 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ def run(self): image_viewer = glue.viewers.image:setup scatter_viewer = glue.viewers.scatter:setup histogram_viewer = glue.viewers.histogram:setup -# table_viewer = glue.viewers.table:setup +table_viewer = glue.viewers.table:setup [console_scripts] glue-config = glue.config_gen:main From 1b2d2b655957495a57667a5180a22c1005f69819 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 8 Aug 2016 11:35:31 +0100 Subject: [PATCH 02/22] Fix QtPy import --- glue/viewers/table/qt/viewer_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 339485ae6..0db8d2876 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -4,7 +4,7 @@ import numpy as np from qtpy.QtCore import Qt -from qtpy import QtGui, QtCore, QtWidgets +from qtpy import QtCore, QtWidgets from qtpy import PYQT5 from glue.core.subset import ElementSubsetState from glue.core.edit_subset_mode import EditSubsetMode From 3ede227d9be7fb7a77924c69eed66c205244e70b Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 10 Aug 2016 14:28:51 +0100 Subject: [PATCH 03/22] Add layer widget --- glue/viewers/table/qt/layer_widget.py | 37 ++++++ glue/viewers/table/qt/layer_widget.ui | 54 +++++++++ glue/viewers/table/qt/viewer_widget.py | 157 +++++++++++++++++-------- 3 files changed, 202 insertions(+), 46 deletions(-) create mode 100644 glue/viewers/table/qt/layer_widget.py create mode 100644 glue/viewers/table/qt/layer_widget.ui diff --git a/glue/viewers/table/qt/layer_widget.py b/glue/viewers/table/qt/layer_widget.py new file mode 100644 index 000000000..9da244a40 --- /dev/null +++ b/glue/viewers/table/qt/layer_widget.py @@ -0,0 +1,37 @@ +import os + +from qtpy import QtWidgets + +from glue.utils.qt.widget_properties import CurrentComboDataProperty +from glue.utils.qt import load_ui, update_combobox + +__all__ = ["LayerWidget"] + + +class LayerWidget(QtWidgets.QWidget): + + layer = CurrentComboDataProperty('ui.combo_active_layer') + + def __init__(self, parent=None, data_viewer=None): + + super(LayerWidget, self).__init__(parent=parent) + + self.ui = load_ui('layer_widget.ui', self, + directory=os.path.dirname(__file__)) + + self._layers = [] + + def __contains__(self, layer): + return layer in self._layers + + def add_layer(self, layer): + self._layers.append(layer) + self._update_combobox() + + def remove_layer(self, layer): + self._layers.remove(layer) + self._update_combobox() + + def _update_combobox(self): + labeldata = [(layer.label, layer) for layer in self._layers] + update_combobox(self.ui.combo_active_layer, labeldata) diff --git a/glue/viewers/table/qt/layer_widget.ui b/glue/viewers/table/qt/layer_widget.ui new file mode 100644 index 000000000..2fbf4a681 --- /dev/null +++ b/glue/viewers/table/qt/layer_widget.ui @@ -0,0 +1,54 @@ + + + Form + + + + 0 + 0 + 287 + 291 + + + + Form + + + + + + + 75 + true + + + + Active layer + + + + + + + QComboBox::AdjustToMinimumContentsLength + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 0db8d2876..82f7ad47c 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -6,12 +6,18 @@ from qtpy.QtCore import Qt from qtpy import QtCore, QtWidgets from qtpy import PYQT5 +from matplotlib.colors import ColorConverter + from glue.core.subset import ElementSubsetState from glue.core.edit_subset_mode import EditSubsetMode from glue.core import message as msg +from glue.utils import nonpartial from glue.utils.qt import load_ui from glue.viewers.common.qt.data_viewer import DataViewer from glue.viewers.common.qt.toolbar import BasicToolbar +from glue.viewers.table.qt.layer_widget import LayerWidget + +COLOR_CONVERTER = ColorConverter() class DataTableModel(QtCore.QAbstractTableModel): @@ -77,6 +83,24 @@ def sort(self, column, ascending): self.layoutChanged.emit() +class BackgroundDelegate(QtWidgets.QStyledItemDelegate): + + def paint(self, painter, option, index): + + # Fill the background before calling the base class paint + # otherwise selected cells would have a white background + background = index.data(Qt.BackgroundRole) + if background is not None and background.canConvert(): + painter.fillRect(option.rect, background.value()) + + QtWidgets.QStyledItemDelegate.paint(self, painter, option, index); + + +def set_table_selection_color(viewer, color): + rgba = COLOR_CONVERTER.to_rgba_array(color)[0] + viewer.setStyleSheet("selection-background-color: rgba({0:.0%}, {1:.0%}, {2:.0%}, {3:.0%});".format(*rgba)); + + class TableWidget(DataViewer): LABEL = "Table Viewer" @@ -112,28 +136,57 @@ def __init__(self, session, parent=None, widget=None): self.model = None self.subset = None self.selected_rows = [] + + self.ui.table.setItemDelegate(BackgroundDelegate()) + + + # The layer widget is used to select which data or subset to show. + # We don't use the default layer list, because in this case we want to + # make sure that only one dataset or subset can be selected at any one + # time. + # self._layer_widget = LayerWidget() + + # Make sure we update the viewer if either the selected layer or the + # column specifying the filename is changed. + # self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( + # nonpartial(self._update_options)) + # self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( + # nonpartial(self._refresh_data)) + # self._options_widget.ui.combo_file_attribute.currentIndexChanged.connect( + # nonpartial(self._refresh_data)) + + # Find out when selection top left has changed + + # For now, we want to make sure that the selection in the table is + # linked to whatever selection is made in the top left data collection + # view. + data_collection_view = self.session.application._layer_widget.ui.layerTree + data_collection_view.selection_changed.connect(nonpartial(self._update_selection)) + + # def layer_view(self): + # return self._layer_widget def register_to_hub(self, hub): super(TableWidget, self).register_to_hub(hub) - dfilter = lambda x: True - dcfilter = lambda x: True - subfilter = lambda x: True - - hub.subscribe(self, msg.SubsetCreateMessage, - handler=self._add_subset, - filter=dfilter) - - hub.subscribe(self, msg.SubsetUpdateMessage, - handler=self._update_subset, - filter=subfilter) - - hub.subscribe(self, msg.SubsetDeleteMessage, - handler=self._remove_subset) - - hub.subscribe(self, msg.DataUpdateMessage, - handler=self.update_window_title) + # dfilter = lambda x: True + # dcfilter = lambda x: True + # subfilter = lambda x: True + # + # hub.subscribe(self, msg.SubsetCreateMessage, + # handler=self._add_subset, + # filter=dfilter) + # + # hub.subscribe(self, msg.SubsetUpdateMessage, + # handler=self._update_subset, + # filter=subfilter) + # + # hub.subscribe(self, msg.SubsetDeleteMessage, + # handler=self._remove_subset) + # + # hub.subscribe(self, msg.DataUpdateMessage, + # handler=self.update_window_title) def _clicked(self, mode): self._broadcast_selection() @@ -149,12 +202,48 @@ def _broadcast_selection(self): mode = EditSubsetMode() mode.update(self.data, subset_state, focus_data=self.data) - def _add_subset(self, message): - self.subset = message.subset - self.selected_rows = self.subset.to_index_list() - self._update_selection() + + def add_data(self, data): + self.data = data + self.set_data(data) + return True + + def add_subset(self, subset): + return True + + def set_data(self, data): + self.setUpdatesEnabled(False) + self.model = DataTableModel(data) + self.model.layoutChanged.connect(self._update_selection) + self.ui.table.setModel(self.model) + self.setUpdatesEnabled(True) + + # def _add_subset(self, message): + # self.subset = message.subset + # self.selected_rows = self.subset.to_index_list() + # self._update_selection() + + # def _update_subset(self, message): + # self._add_subset(message) + # + # def _remove_subset(self, message): + # self.ui.table.clearSelection() + # + # def _update_data(self, message): + # self.set_data(message.data) def _update_selection(self): + """ + Update the selection in the table to reflect the selected subset(s) + """ + + selected_rows = [] + for subset in self.data.edit_subset: + selected_rows.append(subset.to_index_list()) + + # Note that np.unique returns a sorted array + if len(selected_rows) > 0: + selected_rows = np.unique(np.hstack(selected_rows)) self.ui.table.clearSelection() selection_mode = self.ui.table.selectionMode() @@ -162,7 +251,7 @@ def _update_selection(self): # The following is more efficient than just calling selectRow model = self.ui.table.selectionModel() - for index in self.selected_rows: + for index in selected_rows: index = self.model.order[index] model_index = self.model.createIndex(index, 0) model.select(model_index, @@ -170,33 +259,9 @@ def _update_selection(self): self.ui.table.setSelectionMode(selection_mode) - def _update_subset(self, message): - self._add_subset(message) - - def _remove_subset(self, message): - self.ui.table.clearSelection() - - def _update_data(self, message): - self.set_data(message.data) - def unregister(self, hub): pass - def add_data(self, data): - self.data = data - self.set_data(data) - return True - - def add_subset(self, subset): - return True - - def set_data(self, data): - self.setUpdatesEnabled(False) - self.model = DataTableModel(data) - self.model.layoutChanged.connect(self._update_selection) - self.ui.table.setModel(self.model) - self.setUpdatesEnabled(True) - def closeEvent(self, event): """ On close, QT seems to scan through the entire model From cc774b6e6df37c73ad730fe596099d43f81d68eb Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 19 Aug 2016 15:23:51 +0100 Subject: [PATCH 04/22] Added function to do alpha blending of colors, and make sure mpl_to_qt4_color respects existing alpha value if present --- glue/utils/__init__.py | 3 ++- glue/utils/colors.py | 30 ++++++++++++++++++++++++++++++ glue/utils/qt/colors.py | 9 +++++---- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 glue/utils/colors.py diff --git a/glue/utils/__init__.py b/glue/utils/__init__.py index 1acc330d3..15623ad20 100644 --- a/glue/utils/__init__.py +++ b/glue/utils/__init__.py @@ -11,4 +11,5 @@ from .array import * from .matplotlib import * from .misc import * -from .geometry import * \ No newline at end of file +from .geometry import * +from .colors import * diff --git a/glue/utils/colors.py b/glue/utils/colors.py new file mode 100644 index 000000000..36238746a --- /dev/null +++ b/glue/utils/colors.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import, division, print_function + +from matplotlib.colors import ColorConverter + +__all__ = ['alpha_blend_colors'] + +COLOR_CONVERTER = ColorConverter() + + +def alpha_blend_colors(colors, additional_alpha=1.0): + """ + Given a sequence of colors, return the alpha blended color. + + This assumes the last color is the one in front. + """ + + srcr, srcg, srcb, srca = COLOR_CONVERTER.to_rgba(colors[0]) + srca *= additional_alpha + + for color in colors[1:]: + print(srca, srcr, srcg, srcb) + dstr, dstg, dstb, dsta = COLOR_CONVERTER.to_rgba(color) + dsta *= additional_alpha + outa = srca + dsta * (1 - srca) + outr = (srcr * srca + dstr * dsta * (1 - srca)) / outa + outg = (srcg * srca + dstg * dsta * (1 - srca)) / outa + outb = (srcb * srca + dstb * dsta * (1 - srca)) / outa + srca, srcr, srcg, srcb = outa, outr, outg, outb + + return srcr, srcg, srcb, srca diff --git a/glue/utils/qt/colors.py b/glue/utils/qt/colors.py index 56cb48a45..49942a207 100644 --- a/glue/utils/qt/colors.py +++ b/glue/utils/qt/colors.py @@ -17,7 +17,7 @@ 'QColormapCombo'] -def mpl_to_qt4_color(color, alpha=1.0): +def mpl_to_qt4_color(color, alpha=None): """ Convert a matplotlib color stirng into a Qt QColor object @@ -37,9 +37,10 @@ def mpl_to_qt4_color(color, alpha=1.0): return QtGui.QColor(0, 0, 0, 0) cc = ColorConverter() - r, g, b = cc.to_rgb(color) - alpha = max(0, min(255, int(256 * alpha))) - return QtGui.QColor(r * 255, g * 255, b * 255, alpha) + r, g, b, a = cc.to_rgba(color) + if alpha is not None: + a = alpha + return QtGui.QColor(r * 255, g * 255, b * 255, a * 255) def qt4_to_mpl_color(qcolor): From 37b470c7ee55b35730d33e949222ea1f62229195 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 19 Aug 2016 15:24:24 +0100 Subject: [PATCH 05/22] Make table widget able to show multiple selections --- glue/utils/colors.py | 1 - glue/viewers/table/qt/layer_widget.py | 37 ----- glue/viewers/table/qt/layer_widget.ui | 54 ------- glue/viewers/table/qt/viewer_widget.py | 214 +++++++++---------------- 4 files changed, 79 insertions(+), 227 deletions(-) delete mode 100644 glue/viewers/table/qt/layer_widget.py delete mode 100644 glue/viewers/table/qt/layer_widget.ui diff --git a/glue/utils/colors.py b/glue/utils/colors.py index 36238746a..b871f00ea 100644 --- a/glue/utils/colors.py +++ b/glue/utils/colors.py @@ -18,7 +18,6 @@ def alpha_blend_colors(colors, additional_alpha=1.0): srca *= additional_alpha for color in colors[1:]: - print(srca, srcr, srcg, srcb) dstr, dstg, dstb, dsta = COLOR_CONVERTER.to_rgba(color) dsta *= additional_alpha outa = srca + dsta * (1 - srca) diff --git a/glue/viewers/table/qt/layer_widget.py b/glue/viewers/table/qt/layer_widget.py deleted file mode 100644 index 9da244a40..000000000 --- a/glue/viewers/table/qt/layer_widget.py +++ /dev/null @@ -1,37 +0,0 @@ -import os - -from qtpy import QtWidgets - -from glue.utils.qt.widget_properties import CurrentComboDataProperty -from glue.utils.qt import load_ui, update_combobox - -__all__ = ["LayerWidget"] - - -class LayerWidget(QtWidgets.QWidget): - - layer = CurrentComboDataProperty('ui.combo_active_layer') - - def __init__(self, parent=None, data_viewer=None): - - super(LayerWidget, self).__init__(parent=parent) - - self.ui = load_ui('layer_widget.ui', self, - directory=os.path.dirname(__file__)) - - self._layers = [] - - def __contains__(self, layer): - return layer in self._layers - - def add_layer(self, layer): - self._layers.append(layer) - self._update_combobox() - - def remove_layer(self, layer): - self._layers.remove(layer) - self._update_combobox() - - def _update_combobox(self): - labeldata = [(layer.label, layer) for layer in self._layers] - update_combobox(self.ui.combo_active_layer, labeldata) diff --git a/glue/viewers/table/qt/layer_widget.ui b/glue/viewers/table/qt/layer_widget.ui deleted file mode 100644 index 2fbf4a681..000000000 --- a/glue/viewers/table/qt/layer_widget.ui +++ /dev/null @@ -1,54 +0,0 @@ - - - Form - - - - 0 - 0 - 287 - 291 - - - - Form - - - - - - - 75 - true - - - - Active layer - - - - - - - QComboBox::AdjustToMinimumContentsLength - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 82f7ad47c..df988fe1f 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -4,31 +4,37 @@ import numpy as np from qtpy.QtCore import Qt -from qtpy import QtCore, QtWidgets +from qtpy import QtCore, QtGui from qtpy import PYQT5 from matplotlib.colors import ColorConverter -from glue.core.subset import ElementSubsetState -from glue.core.edit_subset_mode import EditSubsetMode +from glue.core.layer_artist import LayerArtistBase from glue.core import message as msg from glue.utils import nonpartial from glue.utils.qt import load_ui from glue.viewers.common.qt.data_viewer import DataViewer from glue.viewers.common.qt.toolbar import BasicToolbar -from glue.viewers.table.qt.layer_widget import LayerWidget +from glue.utils.colors import alpha_blend_colors +from glue.utils.qt import mpl_to_qt4_color COLOR_CONVERTER = ColorConverter() class DataTableModel(QtCore.QAbstractTableModel): - def __init__(self, data): + def __init__(self, table_viewer): super(DataTableModel, self).__init__() - if data.ndim != 1: - raise ValueError("Can only use Table widget for one-dimensional data") - self._data = data + if table_viewer.data.ndim != 1: + raise ValueError("Can only use Table widget for 1D data") + self._table_viewer = table_viewer + self._data = table_viewer.data self.show_hidden = False - self.order = np.arange(data.shape[0]) + self.order = np.arange(self._data.shape[0]) + + def data_changed(self): + top_left = self.index(0, 0) + bottom_right = self.index(self.columnCount(), self.rowCount()) + self.dataChanged.emit(top_left, bottom_right) @property def columns(self): @@ -59,6 +65,7 @@ def data(self, index, role): return None if role == Qt.DisplayRole: + c = self.columns[index.column()] idx = self.order[index.row()] comp = self._data.get_component(c) @@ -71,6 +78,24 @@ def data(self, index, role): else: return str(comp[idx]) + elif role == Qt.BackgroundRole: + + idx = self.order[index.row()] + + # Find all subsets that this index is part of + colors = [] + for layer_artist in self._table_viewer.layers[::-1]: + if layer_artist.visible: + subset = layer_artist.layer + if subset.to_mask(view=slice(idx, idx + 1))[0]: + colors.append(subset.style.color) + + # Blend the colors using alpha blending + if len(colors) > 0: + color = alpha_blend_colors(colors, additional_alpha=0.5) + color = mpl_to_qt4_color(color) + return QtGui.QBrush(color) + def sort(self, column, ascending): c = self.columns[column] comp = self._data.get_component(c) @@ -83,23 +108,16 @@ def sort(self, column, ascending): self.layoutChanged.emit() -class BackgroundDelegate(QtWidgets.QStyledItemDelegate): - - def paint(self, painter, option, index): - - # Fill the background before calling the base class paint - # otherwise selected cells would have a white background - background = index.data(Qt.BackgroundRole) - if background is not None and background.canConvert(): - painter.fillRect(option.rect, background.value()) - - QtWidgets.QStyledItemDelegate.paint(self, painter, option, index); - - -def set_table_selection_color(viewer, color): - rgba = COLOR_CONVERTER.to_rgba_array(color)[0] - viewer.setStyleSheet("selection-background-color: rgba({0:.0%}, {1:.0%}, {2:.0%}, {3:.0%});".format(*rgba)); - +class TableLayerArtist(LayerArtistBase): + def __init__(self, layer, table_viewer): + self._table_viewer = table_viewer + super(TableLayerArtist, self).__init__(layer) + def redraw(self): + self._table_viewer.model.data_changed() + def update(self): + pass + def clear(self): + pass class TableWidget(DataViewer): @@ -131,140 +149,66 @@ def __init__(self, session, parent=None, widget=None): else: hdr.setResizeMode(hdr.Interactive) - self.ui.table.clicked.connect(self._clicked) - self.model = None - self.subset = None - self.selected_rows = [] - - self.ui.table.setItemDelegate(BackgroundDelegate()) - - - # The layer widget is used to select which data or subset to show. - # We don't use the default layer list, because in this case we want to - # make sure that only one dataset or subset can be selected at any one - # time. - # self._layer_widget = LayerWidget() - - # Make sure we update the viewer if either the selected layer or the - # column specifying the filename is changed. - # self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( - # nonpartial(self._update_options)) - # self._layer_widget.ui.combo_active_layer.currentIndexChanged.connect( - # nonpartial(self._refresh_data)) - # self._options_widget.ui.combo_file_attribute.currentIndexChanged.connect( - # nonpartial(self._refresh_data)) - - # Find out when selection top left has changed - - # For now, we want to make sure that the selection in the table is - # linked to whatever selection is made in the top left data collection - # view. - data_collection_view = self.session.application._layer_widget.ui.layerTree - data_collection_view.selection_changed.connect(nonpartial(self._update_selection)) - - # def layer_view(self): - # return self._layer_widget def register_to_hub(self, hub): super(TableWidget, self).register_to_hub(hub) - # dfilter = lambda x: True - # dcfilter = lambda x: True - # subfilter = lambda x: True - # - # hub.subscribe(self, msg.SubsetCreateMessage, - # handler=self._add_subset, - # filter=dfilter) - # - # hub.subscribe(self, msg.SubsetUpdateMessage, - # handler=self._update_subset, - # filter=subfilter) - # - # hub.subscribe(self, msg.SubsetDeleteMessage, - # handler=self._remove_subset) - # - # hub.subscribe(self, msg.DataUpdateMessage, - # handler=self.update_window_title) + def dfilter(x): + return x.sender.data is self.data + + hub.subscribe(self, msg.SubsetCreateMessage, + handler=nonpartial(self._refresh), + filter=dfilter) + + hub.subscribe(self, msg.SubsetUpdateMessage, + handler=nonpartial(self._refresh), + filter=dfilter) - def _clicked(self, mode): - self._broadcast_selection() + hub.subscribe(self, msg.SubsetDeleteMessage, + handler=nonpartial(self._refresh), + filter=dfilter) - def _broadcast_selection(self): + hub.subscribe(self, msg.DataUpdateMessage, + handler=nonpartial(self._refresh), + filter=dfilter) - if self.data is not None: + def _refresh(self): + self._sync_layers() + self.model.data_changed() - model = self.ui.table.selectionModel() - self.selected_rows = [self.model.order[x.row()] for x in model.selectedRows()] - subset_state = ElementSubsetState(self.selected_rows) + def _sync_layers(self): - mode = EditSubsetMode() - mode.update(self.data, subset_state, focus_data=self.data) + # For now we don't show the data in the list because it always has to + # be shown + for layer_artist in self.layers: + if layer_artist.layer not in self.data.subsets: + self._layer_artist_container.remove(layer_artist) + + for subset in self.data.subsets: + if subset not in self._layer_artist_container: + self._layer_artist_container.append(TableLayerArtist(subset, self)) def add_data(self, data): self.data = data - self.set_data(data) - return True - - def add_subset(self, subset): - return True - - def set_data(self, data): self.setUpdatesEnabled(False) - self.model = DataTableModel(data) - self.model.layoutChanged.connect(self._update_selection) + self.model = DataTableModel(self) self.ui.table.setModel(self.model) self.setUpdatesEnabled(True) + self._sync_layers() + return True - # def _add_subset(self, message): - # self.subset = message.subset - # self.selected_rows = self.subset.to_index_list() - # self._update_selection() - - # def _update_subset(self, message): - # self._add_subset(message) - # - # def _remove_subset(self, message): - # self.ui.table.clearSelection() - # - # def _update_data(self, message): - # self.set_data(message.data) - - def _update_selection(self): - """ - Update the selection in the table to reflect the selected subset(s) - """ - - selected_rows = [] - for subset in self.data.edit_subset: - selected_rows.append(subset.to_index_list()) - - # Note that np.unique returns a sorted array - if len(selected_rows) > 0: - selected_rows = np.unique(np.hstack(selected_rows)) - - self.ui.table.clearSelection() - selection_mode = self.ui.table.selectionMode() - self.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - - # The following is more efficient than just calling selectRow - model = self.ui.table.selectionModel() - for index in selected_rows: - index = self.model.order[index] - model_index = self.model.createIndex(index, 0) - model.select(model_index, - QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows) - - self.ui.table.setSelectionMode(selection_mode) + def add_subset(self, subset): + return True def unregister(self, hub): pass def closeEvent(self, event): """ - On close, QT seems to scan through the entire model + On close, Qt seems to scan through the entire model if the data set is big. To sidestep that, we swap out with a tiny data set before closing """ From aee8ed52f46b01d8d4feac5f99b0dc6e3840b567 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 22 Aug 2016 22:25:35 +0100 Subject: [PATCH 06/22] Implement selection in table --- glue/icons/glue_row_select.png | Bin 0 -> 9049 bytes glue/viewers/table/qt/viewer_widget.py | 44 ++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 glue/icons/glue_row_select.png diff --git a/glue/icons/glue_row_select.png b/glue/icons/glue_row_select.png new file mode 100644 index 0000000000000000000000000000000000000000..e3bf06eb940ae5aba69071c01b5aae12a309b482 GIT binary patch literal 9049 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 YDbr&-m;d`q3=E9SX9dY}aif6$0qa$WX#fBK literal 0 HcmV?d00001 diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index df988fe1f..539949cdf 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -4,7 +4,7 @@ import numpy as np from qtpy.QtCore import Qt -from qtpy import QtCore, QtGui +from qtpy import QtCore, QtGui, QtWidgets from qtpy import PYQT5 from matplotlib.colors import ColorConverter @@ -14,6 +14,10 @@ from glue.utils.qt import load_ui from glue.viewers.common.qt.data_viewer import DataViewer from glue.viewers.common.qt.toolbar import BasicToolbar +from glue.viewers.common.qt.mode import CheckableMode +from glue.icons.qt import get_icon +from glue.core.subset import ElementSubsetState +from glue.core.edit_subset_mode import EditSubsetMode from glue.utils.colors import alpha_blend_colors from glue.utils.qt import mpl_to_qt4_color @@ -119,6 +123,25 @@ def update(self): def clear(self): pass +class RowSelectMode(CheckableMode): + + def __init__(self, table=None): + super(RowSelectMode, self).__init__() + self.mode_id = 'ROWSELECT' + self.icon = get_icon('glue_row_select') + self.action_text = 'Select rows' + self.tool_tip = 'Select rows by clicking on rows and pressing enter was once the selection is ready to be applied' + self.table = table + self.deactivate() + + def activate(self): + self.table.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + def deactivate(self): + self.table.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.ui.table.clearSelection() + + class TableWidget(DataViewer): LABEL = "Table Viewer" @@ -150,6 +173,25 @@ def __init__(self, session, parent=None, widget=None): hdr.setResizeMode(hdr.Interactive) self.model = None + self.setup_toolbar() + + def keyPressEvent(self, event): + if event.key() in [Qt.Key_Enter, Qt.Key_Return]: + self.finalize_selection() + super(TableWidget, self).keyPressEvent(event) + + def finalize_selection(self): + model = self.ui.table.selectionModel() + selected_rows = [self.model.order[x.row()] for x in model.selectedRows()] + subset_state = ElementSubsetState(selected_rows) + mode = EditSubsetMode() + mode.update(self._data, subset_state, focus_data=self.data) + self.ui.table.clearSelection() + + def setup_toolbar(self): + self.toolbar = BasicToolbar(self) + self.toolbar.add_mode(RowSelectMode(self)) + self.addToolBar(self.toolbar) def register_to_hub(self, hub): From 084aaa2f145043d41cc0bed4e8d984cdada90b93 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 22 Aug 2016 22:32:18 +0100 Subject: [PATCH 07/22] Added changelog entry --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index c6a8c1a3e..4e0deee57 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,6 +31,8 @@ v0.9.0 (unreleased) * Refactored code related to toolbars in order to make it easier to define toolbars and toolbar modes that aren't Matplotlib-specific. [#1085] +* Added a new table viewer. [#1084] + v0.8.3 (unreleased) ------------------- From a6249cc53cf76f56c4c3a4d613c3c81c3b8ceea8 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 22 Aug 2016 22:48:07 +0100 Subject: [PATCH 08/22] Use status bar to show instructions for selecting rows --- glue/viewers/common/qt/data_viewer.py | 4 ++++ glue/viewers/table/qt/viewer_widget.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/glue/viewers/common/qt/data_viewer.py b/glue/viewers/common/qt/data_viewer.py index 2154c06f0..4d4e4cc53 100644 --- a/glue/viewers/common/qt/data_viewer.py +++ b/glue/viewers/common/qt/data_viewer.py @@ -299,3 +299,7 @@ def window_title(self): def update_window_title(self): self.setWindowTitle(self.window_title) + + def set_status(self, message): + sb = self.statusBar() + sb.showMessage(message) diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 539949cdf..3e904aa01 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -136,10 +136,12 @@ def __init__(self, table=None): def activate(self): self.table.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.table.set_status("CLICK to select, press ENTER to finalize selection") def deactivate(self): self.table.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.ui.table.clearSelection() + self.table.ui.table.clearSelection() + self.table.set_status("") class TableWidget(DataViewer): From 3a74e8fb4c65353755f358ad3fce65a5eae133ce Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 22 Aug 2016 23:23:38 +0100 Subject: [PATCH 09/22] Support saving/loading sessions with table viewers --- glue/viewers/table/qt/viewer_widget.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 3e904aa01..5de4d859b 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -10,6 +10,7 @@ from glue.core.layer_artist import LayerArtistBase from glue.core import message as msg +from glue.core import Data from glue.utils import nonpartial from glue.utils.qt import load_ui from glue.viewers.common.qt.data_viewer import DataViewer @@ -18,6 +19,7 @@ from glue.icons.qt import get_icon from glue.core.subset import ElementSubsetState from glue.core.edit_subset_mode import EditSubsetMode +from glue.core.state import lookup_class_with_patches from glue.utils.colors import alpha_blend_colors from glue.utils.qt import mpl_to_qt4_color @@ -260,3 +262,15 @@ def closeEvent(self, event): d = Data(x=[0]) self.ui.table.setModel(DataTableModel(d)) event.accept() + + def restore_layers(self, rec, context): + # For now this is a bit of a hack, we assume that all subsets saved + # for this viewer are from dataset, so we just get Data object + # then just sync the layers. + for layer in rec: + c = lookup_class_with_patches(layer.pop('_type')) + props = dict((k, context.object(v)) for k, v in layer.items()) + layer = props['layer'] + self.add_data(layer.data) + break + self._sync_layers() From 28d41b0edf12bc9e60577fd08d0d17c5f8a40b4f Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 31 Aug 2016 10:59:43 +0100 Subject: [PATCH 10/22] Remove unused import --- glue/viewers/table/qt/viewer_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 5de4d859b..7f2439955 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -258,7 +258,6 @@ def closeEvent(self, event): if the data set is big. To sidestep that, we swap out with a tiny data set before closing """ - from glue.core import Data d = Data(x=[0]) self.ui.table.setModel(DataTableModel(d)) event.accept() From 2e5d6fd66204490df9173879519b9e249b59ceb8 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 31 Aug 2016 16:11:45 +0100 Subject: [PATCH 11/22] Make it possible for ElementSubsetState to have a Data instance specified to make sure the subset state can't be applied to other datasets. --- glue/core/subset.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/glue/core/subset.py b/glue/core/subset.py index 32fddc180..f27de262b 100644 --- a/glue/core/subset.py +++ b/glue/core/subset.py @@ -879,29 +879,36 @@ def __setgluestate__(cls, rec, context): class ElementSubsetState(SubsetState): - def __init__(self, indices=None): + def __init__(self, indices=None, data=None): super(ElementSubsetState, self).__init__() self._indices = indices + self._data = data @memoize def to_mask(self, data, view=None): - # XXX this is inefficient for views - result = np.zeros(data.shape, dtype=bool) - if self._indices is not None: - result.flat[self._indices] = True - if view is not None: - result = result[view] - return result + if data is self._data or self._data is None: + # XXX this is inefficient for views + result = np.zeros(data.shape, dtype=bool) + if self._indices is not None: + result.flat[self._indices] = True + if view is not None: + result = result[view] + return result + else: + raise IncompatibleAttribute() def copy(self): - return ElementSubsetState(self._indices) + return ElementSubsetState(indices=self._indices, + data=self._data) def __gluestate__(self, context): - return dict(indices=context.do(self._indices)) + return dict(indices=context.do(self._indices), + data=context.do(self._data)) @classmethod def __setgluestate__(cls, rec, context): - return cls(indices=context.object(rec['indices'])) + return cls(indices=context.object(rec['indices']), + data=context.object(rec['data'])) class InequalitySubsetState(SubsetState): From dc7265b2f1b3de5f4deecab3cd261cfd568c646b Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 31 Aug 2016 16:11:56 +0100 Subject: [PATCH 12/22] Update Table viewer to use new toolbar API --- glue/viewers/common/qt/tests/test_toolbar.py | 1 - glue/viewers/table/qt/viewer_widget.py | 51 ++++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/glue/viewers/common/qt/tests/test_toolbar.py b/glue/viewers/common/qt/tests/test_toolbar.py index c9f0cb54d..d2bb39905 100644 --- a/glue/viewers/common/qt/tests/test_toolbar.py +++ b/glue/viewers/common/qt/tests/test_toolbar.py @@ -139,4 +139,3 @@ def test_duplicate_shortcut(): viewer = ExampleViewer2(session) assert len(w) == 1 assert str(w[0].message) == "Tools 'TEST1' and 'TEST2' have the same shortcut ('A'). Ignoring shortcut for 'TEST2'" - diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 7f2439955..1183a7743 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -8,6 +8,7 @@ from qtpy import PYQT5 from matplotlib.colors import ColorConverter +from glue.config import viewer_tool from glue.core.layer_artist import LayerArtistBase from glue.core import message as msg from glue.core import Data @@ -15,13 +16,13 @@ from glue.utils.qt import load_ui from glue.viewers.common.qt.data_viewer import DataViewer from glue.viewers.common.qt.toolbar import BasicToolbar -from glue.viewers.common.qt.mode import CheckableMode -from glue.icons.qt import get_icon +from glue.viewers.common.qt.tool import CheckableTool from glue.core.subset import ElementSubsetState from glue.core.edit_subset_mode import EditSubsetMode from glue.core.state import lookup_class_with_patches from glue.utils.colors import alpha_blend_colors from glue.utils.qt import mpl_to_qt4_color +from glue.core.exceptions import IncompatibleAttribute COLOR_CONVERTER = ColorConverter() @@ -93,8 +94,13 @@ def data(self, index, role): for layer_artist in self._table_viewer.layers[::-1]: if layer_artist.visible: subset = layer_artist.layer - if subset.to_mask(view=slice(idx, idx + 1))[0]: - colors.append(subset.style.color) + try: + if subset.to_mask(view=slice(idx, idx + 1))[0]: + colors.append(subset.style.color) + except IncompatibleAttribute as exc: + layer_artist.disable_invalid_attributes(*exc.args) + else: + layer_artist.enabled = True # Blend the colors using alpha blending if len(colors) > 0: @@ -125,25 +131,26 @@ def update(self): def clear(self): pass -class RowSelectMode(CheckableMode): - def __init__(self, table=None): - super(RowSelectMode, self).__init__() - self.mode_id = 'ROWSELECT' - self.icon = get_icon('glue_row_select') - self.action_text = 'Select rows' - self.tool_tip = 'Select rows by clicking on rows and pressing enter was once the selection is ready to be applied' - self.table = table +@viewer_tool +class RowSelectTool(CheckableTool): + + tool_id = 'table:rowselect' + icon = 'glue_row_select' + action_text = 'CLICK to select, press ENTER to finalize selection' + tool_tip = ('Select rows by clicking on rows and pressing enter ' + 'once the selection is ready to be applied') + + def __init__(self, viewer): + super(RowSelectTool, self).__init__(viewer) self.deactivate() def activate(self): - self.table.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - self.table.set_status("CLICK to select, press ENTER to finalize selection") + self.viewer.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) def deactivate(self): - self.table.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.table.ui.table.clearSelection() - self.table.set_status("") + self.viewer.ui.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.viewer.ui.table.clearSelection() class TableWidget(DataViewer): @@ -151,7 +158,7 @@ class TableWidget(DataViewer): LABEL = "Table Viewer" _toolbar_cls = BasicToolbar - tools = [] + tools = ['table:rowselect'] def __init__(self, session, parent=None, widget=None): @@ -177,7 +184,6 @@ def __init__(self, session, parent=None, widget=None): hdr.setResizeMode(hdr.Interactive) self.model = None - self.setup_toolbar() def keyPressEvent(self, event): if event.key() in [Qt.Key_Enter, Qt.Key_Return]: @@ -187,16 +193,11 @@ def keyPressEvent(self, event): def finalize_selection(self): model = self.ui.table.selectionModel() selected_rows = [self.model.order[x.row()] for x in model.selectedRows()] - subset_state = ElementSubsetState(selected_rows) + subset_state = ElementSubsetState(indices=selected_rows, data=self.data) mode = EditSubsetMode() mode.update(self._data, subset_state, focus_data=self.data) self.ui.table.clearSelection() - def setup_toolbar(self): - self.toolbar = BasicToolbar(self) - self.toolbar.add_mode(RowSelectMode(self)) - self.addToolBar(self.toolbar) - def register_to_hub(self, hub): super(TableWidget, self).register_to_hub(hub) From 6cfb4df7c6c7a5b72fa33f650bebca0c76a614b9 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 31 Aug 2016 16:36:44 +0100 Subject: [PATCH 13/22] More renaming of tool -> mode following toolbar refactor --- .../tools/spectrum_tool/qt/spectrum_tool.py | 4 +- glue/viewers/common/qt/mpl_toolbar.py | 8 +- glue/viewers/common/qt/tests/test_toolbar.py | 36 +++--- glue/viewers/common/qt/toolbar.py | 118 +++++++++--------- glue/viewers/image/qt/viewer_widget.py | 7 +- 5 files changed, 84 insertions(+), 89 deletions(-) diff --git a/glue/plugins/tools/spectrum_tool/qt/spectrum_tool.py b/glue/plugins/tools/spectrum_tool/qt/spectrum_tool.py index cc0636eba..c2f350a13 100644 --- a/glue/plugins/tools/spectrum_tool/qt/spectrum_tool.py +++ b/glue/plugins/tools/spectrum_tool/qt/spectrum_tool.py @@ -834,8 +834,8 @@ def _setup_toolbar(self): tb = MatplotlibViewerToolbar(self.widget) # disable ProfileViewer mouse processing during mouse modes - tb.mode_activated.connect(self.profile.disconnect) - tb.mode_deactivated.connect(self.profile.connect) + tb.tool_activated.connect(self.profile.disconnect) + tb.tool_deactivated.connect(self.profile.connect) self._menu_toggle_action = QtWidgets.QAction("Options", tb) self._menu_toggle_action.setCheckable(True) diff --git a/glue/viewers/common/qt/mpl_toolbar.py b/glue/viewers/common/qt/mpl_toolbar.py index 3a5fd8fdf..98aebb552 100644 --- a/glue/viewers/common/qt/mpl_toolbar.py +++ b/glue/viewers/common/qt/mpl_toolbar.py @@ -153,16 +153,16 @@ def setup_default_modes(self): self._connections = [] - def activate_mode(self, mode): + def activate_tool(self, mode): if isinstance(mode, MouseMode): self._connections.append(self.canvas.mpl_connect('button_press_event', mode.press)) self._connections.append(self.canvas.mpl_connect('motion_notify_event', mode.move)) self._connections.append(self.canvas.mpl_connect('button_release_event', mode.release)) self._connections.append(self.canvas.mpl_connect('key_press_event', mode.key)) - super(MatplotlibViewerToolbar, self).activate_mode(mode) + super(MatplotlibViewerToolbar, self).activate_tool(mode) - def deactivate_mode(self, mode): + def deactivate_tool(self, mode): for connection in self._connections: self.canvas.mpl_disconnect(connection) self._connections = [] - super(MatplotlibViewerToolbar, self).deactivate_mode(mode) + super(MatplotlibViewerToolbar, self).deactivate_tool(mode) diff --git a/glue/viewers/common/qt/tests/test_toolbar.py b/glue/viewers/common/qt/tests/test_toolbar.py index d2bb39905..36f49585b 100644 --- a/glue/viewers/common/qt/tests/test_toolbar.py +++ b/glue/viewers/common/qt/tests/test_toolbar.py @@ -49,8 +49,8 @@ def __init__(self, session, parent=None): def initialize_toolbar(self): super(ExampleViewer, self).initialize_toolbar() - self.mode = MouseModeTest(self, release_callback=self.callback) - self.toolbar.add_tool(self.mode) + self.tool = MouseModeTest(self, release_callback=self.callback) + self.toolbar.add_tool(self.tool) def callback(self, mode): self._called_back = True @@ -71,38 +71,38 @@ def teardown_method(self, method): self.viewer.close() def assert_valid_mode_state(self, target_mode): - for mode in self.viewer.toolbar.buttons: - if mode == target_mode and self.viewer.toolbar.buttons[mode].isCheckable(): - assert self.viewer.toolbar.buttons[mode].isChecked() + for tool_id in self.viewer.toolbar.actions: + if tool_id == target_mode and self.viewer.toolbar.actions[tool_id].isCheckable(): + assert self.viewer.toolbar.actions[tool_id].isChecked() self.viewer.toolbar._active == target_mode else: - assert not self.viewer.toolbar.buttons[mode].isChecked() + assert not self.viewer.toolbar.actions[tool_id].isChecked() def test_callback(self): - self.viewer.toolbar.buttons['mpl:home'].trigger() - self.viewer.mode.release(None) + self.viewer.toolbar.actions['mpl:home'].trigger() + self.viewer.tool.release(None) assert self.viewer._called_back def test_change_mode(self): - self.viewer.toolbar.buttons['mpl:pan'].toggle() - assert self.viewer.toolbar.mode.tool_id == 'mpl:pan' + self.viewer.toolbar.actions['mpl:pan'].toggle() + assert self.viewer.toolbar.active_tool.tool_id == 'mpl:pan' assert self.viewer.toolbar._mpl_nav.mode == 'pan/zoom' - self.viewer.toolbar.buttons['mpl:pan'].toggle() - assert self.viewer.toolbar.mode is None + self.viewer.toolbar.actions['mpl:pan'].toggle() + assert self.viewer.toolbar.active_tool is None assert self.viewer.toolbar._mpl_nav.mode == '' - self.viewer.toolbar.buttons['mpl:zoom'].trigger() - assert self.viewer.toolbar.mode.tool_id == 'mpl:zoom' + self.viewer.toolbar.actions['mpl:zoom'].trigger() + assert self.viewer.toolbar.active_tool.tool_id == 'mpl:zoom' assert self.viewer.toolbar._mpl_nav.mode == 'zoom rect' - self.viewer.toolbar.buttons['mpl:back'].trigger() - assert self.viewer.toolbar.mode is None + self.viewer.toolbar.actions['mpl:back'].trigger() + assert self.viewer.toolbar.active_tool is None assert self.viewer.toolbar._mpl_nav.mode == '' - self.viewer.toolbar.buttons['test'].trigger() - assert self.viewer.toolbar.mode.tool_id == 'test' + self.viewer.toolbar.actions['test'].trigger() + assert self.viewer.toolbar.active_tool.tool_id == 'test' assert self.viewer.toolbar._mpl_nav.mode == '' diff --git a/glue/viewers/common/qt/toolbar.py b/glue/viewers/common/qt/toolbar.py index be8acded0..5b14110aa 100644 --- a/glue/viewers/common/qt/toolbar.py +++ b/glue/viewers/common/qt/toolbar.py @@ -8,9 +8,7 @@ from glue.external import six from glue.core.callback_property import add_callback -from glue.utils import nonpartial -from glue.viewers.common.qt.tool import CheckableTool, Tool -from glue.config import viewer_tool +from glue.viewers.common.qt.tool import CheckableTool from glue.icons.qt import get_icon __all__ = ['BasicToolbar'] @@ -18,8 +16,8 @@ class BasicToolbar(QtWidgets.QToolBar): - mode_activated = QtCore.Signal() - mode_deactivated = QtCore.Signal() + tool_activated = QtCore.Signal() + tool_deactivated = QtCore.Signal() def __init__(self, parent): """ @@ -28,125 +26,123 @@ def __init__(self, parent): super(BasicToolbar, self).__init__(parent=parent) - self.buttons = {} + self.actions = {} self.tools = {} self.setIconSize(QtCore.QSize(25, 25)) self.layout().setSpacing(1) self.setFocusPolicy(Qt.StrongFocus) - self._mode = None - + self._active_tool = None self.setup_default_modes() def setup_default_modes(self): pass @property - def mode(self): - return self._mode + def active_tool(self): + return self._active_tool - @mode.setter - def mode(self, new_mode): + @active_tool.setter + def active_tool(self, new_tool): - old_mode = self._mode + old_tool = self._active_tool - # If the mode is as before, we don't need to do anything - if old_mode is new_mode: + # If the tool is as before, we don't need to do anything + if old_tool is new_tool: return - # Otheriwse, if the mode changes, then we need to disable the previous - # mode... - if old_mode is not None: - self.deactivate_mode(old_mode) - if isinstance(old_mode, CheckableTool): - button = self.buttons[old_mode.tool_id] + # Otheriwse, if the tool changes, then we need to disable the previous + # tool... + if old_tool is not None: + self.deactivate_tool(old_tool) + if isinstance(old_tool, CheckableTool): + button = self.actions[old_tool.tool_id] if button.isChecked(): button.blockSignals(True) button.setChecked(False) button.blockSignals(False) # ... and enable the new one - if new_mode is not None: - self.activate_mode(new_mode) - if isinstance(new_mode, CheckableTool): - button = self.buttons[new_mode.tool_id] + if new_tool is not None: + self.activate_tool(new_tool) + if isinstance(new_tool, CheckableTool): + button = self.actions[new_tool.tool_id] if button.isChecked(): button.blockSignals(True) button.setChecked(True) button.blockSignals(False) - if isinstance(new_mode, CheckableTool): - self._mode = new_mode - self.mode_activated.emit() + if isinstance(new_tool, CheckableTool): + self._active_tool = new_tool + self.active_tool_activated.emit() else: - self._mode = None - self.mode_deactivated.emit() + self._active_tool = None + self.active_tool_deactivated.emit() - def activate_mode(self, mode): - mode.activate() + def activate_tool(self, tool): + tool.activate() - def deactivate_mode(self, mode): - if isinstance(mode, CheckableTool): - mode.deactivate() + def deactivate_tool(self, tool): + if isinstance(tool, CheckableTool): + tool.deactivate() - def add_tool(self, mode): + def add_tool(self, tool): parent = QtWidgets.QToolBar.parent(self) - if isinstance(mode.icon, six.string_types): - if os.path.exists(mode.icon): - icon = QtGui.QIcon(mode.icon) + if isinstance(tool.icon, six.string_types): + if os.path.exists(tool.icon): + icon = QtGui.QIcon(tool.icon) else: - icon = get_icon(mode.icon) + icon = get_icon(tool.icon) else: - icon = mode.icon + icon = tool.icon - action = QtWidgets.QAction(icon, mode.action_text, parent) + action = QtWidgets.QAction(icon, tool.action_text, parent) def toggle(checked): if checked: - self.mode = mode + self.active_tool = tool else: - self.mode = None + self.active_tool = None def trigger(checked): - self.mode = mode + self.active_tool = tool parent.addAction(action) - if isinstance(mode, CheckableTool): + if isinstance(tool, CheckableTool): action.toggled.connect(toggle) else: action.triggered.connect(trigger) shortcut = None - if mode.shortcut is not None: + if tool.shortcut is not None: # Make sure that the keyboard shortcut is unique for m in self.tools.values(): - if mode.shortcut == m.shortcut: + if tool.shortcut == m.shortcut: warnings.warn("Tools '{0}' and '{1}' have the same shortcut " "('{2}'). Ignoring shortcut for " - "'{1}'".format(m.tool_id, mode.tool_id, mode.shortcut)) + "'{1}'".format(m.tool_id, tool.tool_id, tool.shortcut)) break else: - shortcut = mode.shortcut - action.setShortcut(mode.shortcut) + shortcut = tool.shortcut + action.setShortcut(tool.shortcut) action.setShortcutContext(Qt.WidgetShortcut) - if shortcut is None: - action.setToolTip(mode.tool_tip) + action.setToolTip(tool.tool_tip) else: - action.setToolTip(mode.tool_tip + " [shortcut: {0}]".format(shortcut)) + action.setToolTip(tool.tool_tip + " [shortcut: {0}]".format(shortcut)) - action.setCheckable(isinstance(mode, CheckableTool)) - self.buttons[mode.tool_id] = action + action.setCheckable(isinstance(tool, CheckableTool)) + self.actions[tool.tool_id] = action - menu_actions = mode.menu_actions() + menu_actions = tool.menu_actions() if len(menu_actions) > 0: menu = QtWidgets.QMenu(self) - for ma in mode.menu_actions(): + for ma in tool.menu_actions(): ma.setParent(self) menu.addAction(ma) action.setMenu(menu) @@ -154,12 +150,12 @@ def trigger(checked): self.addAction(action) - # Bind tool visibility to mode.enabled + # Bind tool visibility to tool.enabled def toggle(state): action.setVisible(state) action.setEnabled(state) - add_callback(mode, 'enabled', toggle) + add_callback(tool, 'enabled', toggle) - self.tools[mode.tool_id] = mode + self.tools[tool.tool_id] = tool return action diff --git a/glue/viewers/image/qt/viewer_widget.py b/glue/viewers/image/qt/viewer_widget.py index fc8dc46a9..6cb3dc338 100644 --- a/glue/viewers/image/qt/viewer_widget.py +++ b/glue/viewers/image/qt/viewer_widget.py @@ -378,9 +378,9 @@ def initialize_toolbar(self): # connect viewport update buttons to client commands to # allow resampling cl = self.client - self.toolbar.buttons['mpl:home'].triggered.connect(nonpartial(cl.check_update)) - self.toolbar.buttons['mpl:forward'].triggered.connect(nonpartial(cl.check_update)) - self.toolbar.buttons['mpl:back'].triggered.connect(nonpartial(cl.check_update)) + self.toolbar.actions['mpl:home'].triggered.connect(nonpartial(cl.check_update)) + self.toolbar.actions['mpl:forward'].triggered.connect(nonpartial(cl.check_update)) + self.toolbar.actions['mpl:back'].triggered.connect(nonpartial(cl.check_update)) def paintEvent(self, event): super(ImageWidget, self).paintEvent(event) @@ -530,4 +530,3 @@ def initialize_toolbar(self): self.toolbar.add_tool(mode) self.addToolBar(self.toolbar) - From 3da789aeaf400d5b2e33a9b2fa03ea730508c831 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 31 Aug 2016 17:04:59 +0100 Subject: [PATCH 14/22] Expanded unit test for Table viewer --- .../table/qt/tests/test_viewer_widget.py | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/glue/viewers/table/qt/tests/test_viewer_widget.py b/glue/viewers/table/qt/tests/test_viewer_widget.py index 502ebd5e8..8ea9ebe1f 100644 --- a/glue/viewers/table/qt/tests/test_viewer_widget.py +++ b/glue/viewers/table/qt/tests/test_viewer_widget.py @@ -4,6 +4,7 @@ from qtpy.QtCore import Qt from glue.core import Data, DataCollection, Session +from glue.utils.qt import qt4_to_mpl_color from ..viewer_widget import DataTableModel, TableWidget @@ -51,12 +52,91 @@ def test_data_2d(self): assert float(result) == self.data[c].ravel()[j] +def check_values_and_color(model, data, colors): + + for i in range(len(colors)): + + for j, colname in enumerate('abc'): + + # Get index of cell + idx = model.index(i, j) + + # Check values + value = model.data(idx, Qt.DisplayRole) + assert value == str(data[colname][i]) + + # Check colors + brush = model.data(idx, Qt.BackgroundRole) + if colors[i] is None: + assert brush is None + else: + assert qt4_to_mpl_color(brush.color()) == colors[i] + + def test_table_widget(): - d = Data(a=[1,2,3,4,5], b=[1.2, 3.3, 4.5, 3.2, 2.2], c=['a','b','c','d','e']) + # TODO: add tests for doing the selection interactively + + d = Data(a=[1, 2, 3, 4, 5], + b=[3.2, 1.2, 4.5, 3.3, 2.2], + c=['e', 'b', 'c', 'a', 'f']) dc = DataCollection([d]) session = Session(dc, hub=dc.hub) widget = TableWidget(session) + widget.register_to_hub(dc.hub) widget.add_data(d) widget.show() + + sg1 = dc.new_subset_group('D >= 3', d.id['a'] <= 3) + sg1.style.color = '#aa0000' + sg2 = dc.new_subset_group('1 < D < 4', (d.id['a'] > 1) & (d.id['a'] < 4)) + sg2.style.color = '#0000cc' + + model = widget.ui.table.model() + + data = { + 'a': [1, 2, 3, 4, 5], + 'b': [3.2, 1.2, 4.5, 3.3, 2.2], + 'c': ['e', 'b', 'c', 'a', 'f'] + } + + colors = ['#aa0000', '#380088', '#380088', None, None] + + check_values_and_color(model, data, colors) + + model.sort(1, Qt.AscendingOrder) + + data = { + 'a': [2, 5, 1, 4, 3], + 'b': [1.2, 2.2, 3.2, 3.3, 4.5], + 'c': ['b', 'f', 'e', 'a', 'c'] + } + + colors = ['#380088', None, '#aa0000', None, '#380088'] + + check_values_and_color(model, data, colors) + + model.sort(2, Qt.AscendingOrder) + + data = { + 'a': [4, 2, 3, 1, 5], + 'b': [3.3, 1.2, 4.5, 3.2, 2.2], + 'c': ['a', 'b', 'c', 'e', 'f'] + } + + colors = [None, '#380088', '#380088', '#aa0000', None] + + check_values_and_color(model, data, colors) + + model.sort(0, Qt.DescendingOrder) + + data = { + 'a': [5, 4, 3, 2, 1], + 'b': [2.2, 3.3, 4.5, 1.2, 3.2], + 'c': ['f', 'a', 'c', 'b', 'e'] + } + + colors = [None, None, '#380088', '#380088', '#aa0000'] + + check_values_and_color(model, data, colors) \ No newline at end of file From fc994b9f07e6adf210e3a6ca332f1686e4ae5df9 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 31 Aug 2016 21:31:08 +0100 Subject: [PATCH 15/22] Fix following event renaming --- glue/viewers/common/qt/toolbar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glue/viewers/common/qt/toolbar.py b/glue/viewers/common/qt/toolbar.py index 5b14110aa..bc9e42931 100644 --- a/glue/viewers/common/qt/toolbar.py +++ b/glue/viewers/common/qt/toolbar.py @@ -73,10 +73,10 @@ def active_tool(self, new_tool): if isinstance(new_tool, CheckableTool): self._active_tool = new_tool - self.active_tool_activated.emit() + self.tool_activated.emit() else: self._active_tool = None - self.active_tool_deactivated.emit() + self.tool_deactivated.emit() def activate_tool(self, tool): tool.activate() From 536b448d68dc2a384635d4d73ae6ae9b2234f52e Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 31 Aug 2016 21:31:48 +0100 Subject: [PATCH 16/22] Expanded Table unit test to include selection events --- .../table/qt/tests/test_viewer_widget.py | 111 ++++++++++++++++-- 1 file changed, 104 insertions(+), 7 deletions(-) diff --git a/glue/viewers/table/qt/tests/test_viewer_widget.py b/glue/viewers/table/qt/tests/test_viewer_widget.py index 8ea9ebe1f..da0095411 100644 --- a/glue/viewers/table/qt/tests/test_viewer_widget.py +++ b/glue/viewers/table/qt/tests/test_viewer_widget.py @@ -1,13 +1,20 @@ from __future__ import absolute_import, division, print_function import pytest +import numpy as np +from qtpy import QtCore, QtGui +from glue.utils.qt import get_qapp from qtpy.QtCore import Qt from glue.core import Data, DataCollection, Session from glue.utils.qt import qt4_to_mpl_color +from glue.app.qt import GlueApplication from ..viewer_widget import DataTableModel, TableWidget +from glue.core.edit_subset_mode import (EditSubsetMode, AndNotMode, OrMode, + ReplaceMode) + class TestDataTableModel(): @@ -75,26 +82,36 @@ def check_values_and_color(model, data, colors): def test_table_widget(): - # TODO: add tests for doing the selection interactively + # Start off by creating a glue application instance with a table viewer and + # some data pre-loaded. + + app = get_qapp() d = Data(a=[1, 2, 3, 4, 5], b=[3.2, 1.2, 4.5, 3.3, 2.2], c=['e', 'b', 'c', 'a', 'f']) + dc = DataCollection([d]) - session = Session(dc, hub=dc.hub) - widget = TableWidget(session) - widget.register_to_hub(dc.hub) + gapp = GlueApplication(dc) + + widget = gapp.new_data_viewer(TableWidget) widget.add_data(d) - widget.show() - sg1 = dc.new_subset_group('D >= 3', d.id['a'] <= 3) + subset_mode = EditSubsetMode() + + # Create two subsets + + sg1 = dc.new_subset_group('D <= 3', d.id['a'] <= 3) sg1.style.color = '#aa0000' sg2 = dc.new_subset_group('1 < D < 4', (d.id['a'] > 1) & (d.id['a'] < 4)) sg2.style.color = '#0000cc' model = widget.ui.table.model() + # We now check what the data and colors of the table are, and try various + # sorting methods to make sure that things are still correct. + data = { 'a': [1, 2, 3, 4, 5], 'b': [3.2, 1.2, 4.5, 3.3, 2.2], @@ -139,4 +156,84 @@ def test_table_widget(): colors = [None, None, '#380088', '#380088', '#aa0000'] - check_values_and_color(model, data, colors) \ No newline at end of file + check_values_and_color(model, data, colors) + + model.sort(0, Qt.AscendingOrder) + + # We now modify the subsets using the table. + + selection = widget.ui.table.selectionModel() + + widget.toolbar.actions['table:rowselect'].toggle() + + def press_key(key): + event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key, Qt.NoModifier) + app.postEvent(widget.ui.table, event) + app.processEvents() + + app.processEvents() + + # We now use key presses to navigate down to the third row + + press_key(Qt.Key_Tab) + press_key(Qt.Key_Down) + press_key(Qt.Key_Down) + + indices = selection.selectedRows() + + # We make sure that the third row is selected + + assert len(indices) == 1 + assert indices[0].row() == 2 + + # At this point, the subsets haven't changed yet + + np.testing.assert_equal(d.subsets[0].to_mask(), [1, 1, 1, 0, 0]) + np.testing.assert_equal(d.subsets[1].to_mask(), [0, 1, 1, 0, 0]) + + # We specify that we are editing the second subset, and use a 'not' logical + # operation to remove the currently selected line from the second subset. + + d.edit_subset = [d.subsets[1]] + + subset_mode.mode = AndNotMode + + press_key(Qt.Key_Enter) + + np.testing.assert_equal(d.subsets[0].to_mask(), [1, 1, 1, 0, 0]) + np.testing.assert_equal(d.subsets[1].to_mask(), [0, 1, 0, 0, 0]) + + # At this point, the selection should be cleared + + indices = selection.selectedRows() + assert len(indices) == 0 + + # We move to the fourth row and now do an 'or' selection with the first + # subset. + + press_key(Qt.Key_Down) + + subset_mode.mode = OrMode + + d.edit_subset = [d.subsets[0]] + + press_key(Qt.Key_Enter) + + np.testing.assert_equal(d.subsets[0].to_mask(), [1, 1, 1, 1, 0]) + np.testing.assert_equal(d.subsets[1].to_mask(), [0, 1, 0, 0, 0]) + + # Finally we move to the fifth row and deselect all subsets so that + # pressing enter now creates a new subset. + + press_key(Qt.Key_Down) + + subset_mode.mode = ReplaceMode + + d.edit_subset = None + + press_key(Qt.Key_Enter) + + np.testing.assert_equal(d.subsets[0].to_mask(), [1, 1, 1, 1, 0]) + np.testing.assert_equal(d.subsets[1].to_mask(), [0, 1, 0, 0, 0]) + np.testing.assert_equal(d.subsets[2].to_mask(), [0, 0, 0, 0, 1]) + From 0e162cce158a627448696440fe75492c8645556e Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Sep 2016 09:38:25 +0100 Subject: [PATCH 17/22] Fix saving/loading of categorical components --- glue/core/state.py | 47 ++++++++++++++++++++++++++++++++--- glue/core/tests/test_state.py | 10 ++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/glue/core/state.py b/glue/core/state.py index f8f1f01bb..a405c574c 100644 --- a/glue/core/state.py +++ b/glue/core/state.py @@ -69,8 +69,10 @@ def load(rec, context) from glue.external import six from glue import core -from glue.core.data import (Data, Component, ComponentID, DerivedComponent, - CoordinateComponent) +from glue.core.data import Data +from glue.core.component_id import ComponentID +from glue.core.component import (Component, CategoricalComponent, + DerivedComponent, CoordinateComponent) from glue.core.subset import (OPSYM, SYMOP, CompositeSubsetState, SubsetState, Subset, RoiSubsetState, InequalitySubsetState, RangeSubsetState) @@ -701,9 +703,9 @@ def _save_data_3(data, context): @loader(Data, version=3) def _load_data_3(rec, context): result = _load_data_2(rec, context) - yield result result._key_joins = dict((context.object(k), (context.object(v0), context.object(v1))) for k, v0, v1 in rec['_key_joins']) + return result @saver(Data, version=4) @@ -719,11 +721,25 @@ def save_cid_tuple(cids): @loader(Data, version=4) def _load_data_4(rec, context): result = _load_data_2(rec, context) - yield result def load_cid_tuple(cids): return tuple(context.object(cid) for cid in cids) result._key_joins = dict((context.object(k), (load_cid_tuple(v0), load_cid_tuple(v1))) for k, v0, v1 in rec['_key_joins']) + return result + +@saver(Data, version=5) +def _save_data_5(data, context): + result = _save_data_4(data, context) + result['uuid'] = data.uuid + return result + + +@loader(Data, version=5) +def _load_data_5(rec, context): + result = _load_data_4(rec, context) + result.uuid = rec['uuid'] + return result + @saver(ComponentID) def _save_component_id(cid, context): @@ -755,6 +771,29 @@ def _load_component(rec, context): return Component(data=context.object(rec['data']), units=rec['units']) +@saver(CategoricalComponent) +def _save_categorical_component(component, context): + + if not context.include_data and hasattr(component, '_load_log'): + log = component._load_log + return dict(log=context.id(log), + log_item=log.id(component)) + + return dict(categorical_data=context.do(component.labels), + categories=context.do(component.categories), + jitter_method=context.do(component._jitter_method), + units=component.units) + + +@loader(CategoricalComponent) +def _load_categorical_component(rec, context): + if 'log' in rec: + return context.object(rec['log']).component(rec['log_item']) + + return CategoricalComponent(categorical_data=context.object(rec['categorical_data']), + categories=context.object(rec['categories']), + jitter=context.object(rec['jitter_method']), + units=rec['units']) @saver(DerivedComponent) def _save_derived_component(component, context): diff --git a/glue/core/tests/test_state.py b/glue/core/tests/test_state.py index bd3d17d99..a1831f83e 100644 --- a/glue/core/tests/test_state.py +++ b/glue/core/tests/test_state.py @@ -8,6 +8,7 @@ from glue.external import six from glue import core +from glue.core.component import CategoricalComponent from glue.tests.helpers import requires_astropy, make_file from ..data_factories import load_data @@ -233,6 +234,15 @@ def test_matplotlib_cmap(): assert clone(cm.gist_heat) is cm.gist_heat +def test_categorical_component(): + c = CategoricalComponent(['a','b','c','a','b'], categories=['a','b','c']) + c2 = clone(c) + assert isinstance(c2, CategoricalComponent) + np.testing.assert_array_equal(c.data, [0, 1, 2, 0, 1]) + np.testing.assert_array_equal(c.labels, ['a','b','c','a','b']) + np.testing.assert_array_equal(c.categories, ['a','b','c']) + + class DummyClass(object): pass From bc3318e6d480e68fcb983f143f58819c81112e5d Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Sep 2016 09:38:52 +0100 Subject: [PATCH 18/22] Expanded unit test for Table viewer to include saving/loading session --- .../table/qt/tests/test_viewer_widget.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/glue/viewers/table/qt/tests/test_viewer_widget.py b/glue/viewers/table/qt/tests/test_viewer_widget.py index da0095411..4b4bc24c1 100644 --- a/glue/viewers/table/qt/tests/test_viewer_widget.py +++ b/glue/viewers/table/qt/tests/test_viewer_widget.py @@ -80,7 +80,7 @@ def check_values_and_color(model, data, colors): assert qt4_to_mpl_color(brush.color()) == colors[i] -def test_table_widget(): +def test_table_widget(tmpdir): # Start off by creating a glue application instance with a table viewer and # some data pre-loaded. @@ -237,3 +237,31 @@ def press_key(key): np.testing.assert_equal(d.subsets[1].to_mask(), [0, 1, 0, 0, 0]) np.testing.assert_equal(d.subsets[2].to_mask(), [0, 0, 0, 0, 1]) + # Make the color for the new subset deterministic + dc.subset_groups[2].style.color = '#bababa' + + # Now finally check saving and restoring session + + 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() + + data = { + 'a': [1, 2, 3, 4, 5], + 'b': [3.2, 1.2, 4.5, 3.3, 2.2], + 'c': ['e', 'b', 'c', 'a', 'f'] + } + + # Need to take into account new selections above + colors = ['#aa0000', '#380088', '#aa0000', "#aa0000", "#bababa"] + + check_values_and_color(model2, data, colors) From d13731fb00700f8a81ad5fa134ea3a3cc15bb554 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Sep 2016 09:52:05 +0100 Subject: [PATCH 19/22] Add a .uuid attribute for Data objects - this can then be used to refer to data objects in cases where the serialization would cause circular references. --- glue/core/data.py | 6 ++++++ glue/core/state.py | 13 +------------ glue/core/subset.py | 19 ++++++++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/glue/core/data.py b/glue/core/data.py index 6ffa98ec7..602a69322 100644 --- a/glue/core/data.py +++ b/glue/core/data.py @@ -2,6 +2,7 @@ from collections import OrderedDict +import uuid import numpy as np import pandas as pd @@ -100,6 +101,11 @@ def __init__(self, label="", **kwargs): self._key_joins = {} + # To avoid circular references when saving objects with references to + # the data, we make sure that all Data objects have a UUID that can + # uniquely identify them. + self.uuid = str(uuid.uuid4()) + @property def subsets(self): """ diff --git a/glue/core/state.py b/glue/core/state.py index a405c574c..e6f43989b 100644 --- a/glue/core/state.py +++ b/glue/core/state.py @@ -715,6 +715,7 @@ def save_cid_tuple(cids): return tuple(context.id(cid) for cid in cids) result['_key_joins'] = [[context.id(k), save_cid_tuple(v0), save_cid_tuple(v1)] for k, (v0, v1) in data._key_joins.items()] + result['uuid'] = data.uuid return result @@ -725,18 +726,6 @@ def load_cid_tuple(cids): return tuple(context.object(cid) for cid in cids) result._key_joins = dict((context.object(k), (load_cid_tuple(v0), load_cid_tuple(v1))) for k, v0, v1 in rec['_key_joins']) - return result - -@saver(Data, version=5) -def _save_data_5(data, context): - result = _save_data_4(data, context) - result['uuid'] = data.uuid - return result - - -@loader(Data, version=5) -def _load_data_5(rec, context): - result = _load_data_4(rec, context) result.uuid = rec['uuid'] return result diff --git a/glue/core/subset.py b/glue/core/subset.py index f27de262b..801565547 100644 --- a/glue/core/subset.py +++ b/glue/core/subset.py @@ -882,11 +882,14 @@ class ElementSubsetState(SubsetState): def __init__(self, indices=None, data=None): super(ElementSubsetState, self).__init__() self._indices = indices - self._data = data + if data is None: + self._data_uuid = None + else: + self._data_uuid = data.uuid @memoize def to_mask(self, data, view=None): - if data is self._data or self._data is None: + if data.uuid == self._data_uuid or self._data_uuid is None: # XXX this is inefficient for views result = np.zeros(data.shape, dtype=bool) if self._indices is not None: @@ -898,17 +901,19 @@ def to_mask(self, data, view=None): raise IncompatibleAttribute() def copy(self): - return ElementSubsetState(indices=self._indices, - data=self._data) + state = ElementSubsetState(indices=self._indices) + state._data_uuid = self._data_uuid + return state def __gluestate__(self, context): return dict(indices=context.do(self._indices), - data=context.do(self._data)) + data_uuid=self._data_uuid) @classmethod def __setgluestate__(cls, rec, context): - return cls(indices=context.object(rec['indices']), - data=context.object(rec['data'])) + state = cls(indices=context.object(rec['indices'])) + state._data_uuid = rec['data_uuid'] + return state class InequalitySubsetState(SubsetState): From 054ce18b47e28f09221adbc9e57f2ada9ca7fb20 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Sep 2016 09:57:22 +0100 Subject: [PATCH 20/22] Added changelog entry [ci skip] --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 4e0deee57..c6f733a11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,8 @@ v0.9.0 (unreleased) * Added a new table viewer. [#1084] +* Fix saving/loading of categorical components. [#1084] + v0.8.3 (unreleased) ------------------- From ff06e849113ce1319d76a7ee989b5a6d8d8ec3f3 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Sep 2016 10:22:36 +0100 Subject: [PATCH 21/22] Make it possible to define ``status_tip`` on CheckableTool, to show instructions in status bar. --- CHANGES.md | 2 ++ doc/customizing_guide/toolbar.rst | 11 +++++++---- glue/viewers/common/qt/mouse_mode.py | 16 ++++++++++++---- glue/viewers/common/qt/tool.py | 4 +++- glue/viewers/common/qt/toolbar.py | 2 ++ glue/viewers/table/qt/viewer_widget.py | 3 ++- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c6f733a11..aba890052 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,8 @@ v0.9.0 (unreleased) * Fix saving/loading of categorical components. [#1084] +* Make it possible for tools to define a status bar message. [#1084] + v0.8.3 (unreleased) ------------------- diff --git a/doc/customizing_guide/toolbar.rst b/doc/customizing_guide/toolbar.rst index 7b322635e..6350ef497 100644 --- a/doc/customizing_guide/toolbar.rst +++ b/doc/customizing_guide/toolbar.rst @@ -48,8 +48,8 @@ The class-level variables set at the start of the class are as follows: tool that has the same ``tool_id`` as an existing tool already implemented in glue, you will get an error. -* ``action_text``: a string describing the tool. This is shown in the status bar - at the bottom of the viewer whenever the button is active. +* ``action_text``: a string describing the tool. This is not currently used, + but would be the text that would appear if the tool was accessible by a menu. * ``tool_tip``: this should be a string that will be shown when the user hovers above the button in the toolbar. This can include instructions on how to use @@ -78,7 +78,7 @@ Checkable tools ^^^^^^^^^^^^^^^ The basic structure for a checkable tool is similar to the above, but with an -additional ``deactivate`` method: +additional ``deactivate`` method, and a ``status_tip`` attribute: .. code:: python @@ -92,6 +92,7 @@ additional ``deactivate`` method: tool_id = 'custom_tool' action_text = 'Does cool stuff' tool_tip = 'Does cool stuff' + status_tip = 'Instructions on what to do now' shortcut = 'D' def __init__(self, viewer): @@ -109,7 +110,9 @@ additional ``deactivate`` method: When the tool icon is pressed, the ``activate`` method is called, and when the button is unchecked (either by clicking on it again, or if the user clicks on another tool icon), the ``deactivate`` method is called. As before, when the -viewer is closed, the ``close`` method is called. +viewer is closed, the ``close`` method is called. The ``status_tip`` is a +message shown in the status bar of the viewer when the tool is active. This can +be used to provide instructions to the user as to what they should do next. Drop-down menus ^^^^^^^^^^^^^^^ diff --git a/glue/viewers/common/qt/mouse_mode.py b/glue/viewers/common/qt/mouse_mode.py index 210aa44b0..aa5ed7c17 100644 --- a/glue/viewers/common/qt/mouse_mode.py +++ b/glue/viewers/common/qt/mouse_mode.py @@ -361,6 +361,8 @@ class PolyMode(ClickRoiMode): tool_tip = ('Lasso a region of interest\n' ' ENTER accepts the path\n' ' ESCAPE clears the path') + status_tip = ('CLICK and DRAG to define lasso, CLICK multiple times to ' + 'define polygon, ENTER to finalize, ESCAPE to cancel') shortcut = 'G' def __init__(self, viewer, **kwargs): @@ -368,6 +370,8 @@ def __init__(self, viewer, **kwargs): self._roi_tool = qt_roi.QtPolygonalROI(self._axes) +# TODO: determine why LassoMode exists since it's the same as PolyMode? + @viewer_tool class LassoMode(RoiMode): """ @@ -375,9 +379,13 @@ class LassoMode(RoiMode): """ icon = 'glue_lasso' - tool_id = 'Lasso' + tool_id = 'select:lasso' action_text = 'Polygonal ROI' - tool_tip = 'Lasso a region of interest' + tool_tip = ('Lasso a region of interest\n' + ' ENTER accepts the path\n' + ' ESCAPE clears the path') + status_tip = ('CLICK and DRAG to define lasso, CLICK multiple times to ' + 'define polygon, ENTER to finalize, ESCAPE to cancel') shortcut = 'L' def __init__(self, viewer, **kwargs): @@ -395,7 +403,7 @@ class HRangeMode(RoiMode): icon = 'glue_xrange_select' tool_id = 'select:xrange' - action_text = 'select:xrange' + action_text = 'X range' tool_tip = 'Select a range of x values' shortcut = 'X' @@ -414,7 +422,7 @@ class VRangeMode(RoiMode): icon = 'glue_yrange_select' tool_id = 'select:yrange' - action_text = 'select:yrange' + action_text = 'Y range' tool_tip = 'Select a range of y values' shortcut = 'Y' diff --git a/glue/viewers/common/qt/tool.py b/glue/viewers/common/qt/tool.py index 2752d4177..1567c4ebb 100644 --- a/glue/viewers/common/qt/tool.py +++ b/glue/viewers/common/qt/tool.py @@ -15,8 +15,9 @@ class Tool(object): * icon : QIcon object * tool_id : a short name for the tool - * action_text : the action title + * action_text : the action title (used if the tool is made available in a menu) * tool_tip : a tip that is shown when the user hovers over the icon + * status_tip : a tip that is shown in the status bar when the tool is active * shortcut : keyboard shortcut to toggle the tool """ @@ -26,6 +27,7 @@ class Tool(object): tool_id = None action_text = None tool_tip = None + status_tip = None shortcut = None def __init__(self, viewer=None): diff --git a/glue/viewers/common/qt/toolbar.py b/glue/viewers/common/qt/toolbar.py index bc9e42931..08a8bc8d6 100644 --- a/glue/viewers/common/qt/toolbar.py +++ b/glue/viewers/common/qt/toolbar.py @@ -73,9 +73,11 @@ def active_tool(self, new_tool): if isinstance(new_tool, CheckableTool): self._active_tool = new_tool + self.parent().set_status(new_tool.status_tip) self.tool_activated.emit() else: self._active_tool = None + self.parent().set_status('') self.tool_deactivated.emit() def activate_tool(self, tool): diff --git a/glue/viewers/table/qt/viewer_widget.py b/glue/viewers/table/qt/viewer_widget.py index 1183a7743..4a530223a 100644 --- a/glue/viewers/table/qt/viewer_widget.py +++ b/glue/viewers/table/qt/viewer_widget.py @@ -137,9 +137,10 @@ class RowSelectTool(CheckableTool): tool_id = 'table:rowselect' icon = 'glue_row_select' - action_text = 'CLICK to select, press ENTER to finalize selection' + action_text = 'Select row(s)' tool_tip = ('Select rows by clicking on rows and pressing enter ' 'once the selection is ready to be applied') + status_tip = 'CLICK to select, press ENTER to finalize selection' def __init__(self, viewer): super(RowSelectTool, self).__init__(viewer) From 2c97cc512fbf6b93e44999d50f222e85cc270594 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Sep 2016 10:37:57 +0100 Subject: [PATCH 22/22] Fix tests --- glue/core/state.py | 4 ++-- glue/core/tests/test_subset.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/glue/core/state.py b/glue/core/state.py index e6f43989b..f7c640260 100644 --- a/glue/core/state.py +++ b/glue/core/state.py @@ -703,9 +703,9 @@ def _save_data_3(data, context): @loader(Data, version=3) def _load_data_3(rec, context): result = _load_data_2(rec, context) + yield result result._key_joins = dict((context.object(k), (context.object(v0), context.object(v1))) for k, v0, v1 in rec['_key_joins']) - return result @saver(Data, version=4) @@ -722,12 +722,12 @@ def save_cid_tuple(cids): @loader(Data, version=4) def _load_data_4(rec, context): result = _load_data_2(rec, context) + yield result def load_cid_tuple(cids): return tuple(context.object(cid) for cid in cids) result._key_joins = dict((context.object(k), (load_cid_tuple(v0), load_cid_tuple(v1))) for k, v0, v1 in rec['_key_joins']) result.uuid = rec['uuid'] - return result @saver(ComponentID) diff --git a/glue/core/tests/test_subset.py b/glue/core/tests/test_subset.py index 7fc0d2217..a0aa3b308 100644 --- a/glue/core/tests/test_subset.py +++ b/glue/core/tests/test_subset.py @@ -306,6 +306,7 @@ class TestSubsetIo(object): def setup_method(self, method): self.data = MagicMock(spec=Data) self.data.shape = (4, 4) + self.data.uuid = 'abcde' self.subset = Subset(self.data) inds = np.array([1, 2, 3]) self.subset.subset_state = ElementSubsetState(indices=inds)