diff --git a/CHANGES.rst b/CHANGES.rst index 2b1a782015..a1c278dc7a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,7 @@ New Features ------------ -* New design for viewer legend. [#3220, #3254, #3263] +* New design for viewer legend. [#3220, #3254, #3263, #3264] Cubeviz ^^^^^^^ diff --git a/jdaviz/app.py b/jdaviz/app.py index bcc0c405b2..007f907d37 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -133,6 +133,8 @@ def to_unit(self, data, cid, values, original_units, target_units): 'j-plugin-popout': 'components/plugin_popout.vue', 'j-multiselect-toggle': 'components/multiselect_toggle.vue', 'j-subset-icon': 'components/subset_icon.vue', + 'j-plugin-live-results-icon': 'components/plugin_live_results_icon.vue', + 'j-child-layer-icon': 'components/child_layer_icon.vue', 'plugin-previews-temp-disabled': 'components/plugin_previews_temp_disabled.vue', # noqa 'plugin-table': 'components/plugin_table.vue', 'plugin-dataset-select': 'components/plugin_dataset_select.vue', @@ -152,7 +154,9 @@ def to_unit(self, data, cid, values, original_units, target_units): 'plugin-color-picker': 'components/plugin_color_picker.vue', 'plugin-input-header': 'components/plugin_input_header.vue', 'glue-state-sync-wrapper': 'components/glue_state_sync_wrapper.vue', - 'data-menu-add-data': 'components/data_menu_add_data.vue'} + 'data-menu-add-data': 'components/data_menu_add_data.vue', + 'data-menu-remove': 'components/data_menu_remove.vue', + 'data-menu-subset-edit': 'components/data_menu_subset_edit.vue'} _verbosity_levels = ('debug', 'info', 'warning', 'error') @@ -2287,7 +2291,7 @@ def vue_data_item_remove(self, event): data_label = event['item_name'] data = self.data_collection[data_label] orientation_plugin = self._jdaviz_helper.plugins.get("Orientation") - if orientation_plugin is not None: + if orientation_plugin is not None and orientation_plugin.align_by == "WCS": from jdaviz.configs.imviz.plugins.orientation.orientation import base_wcs_layer_label orient = orientation_plugin.orientation.selected if orient == data_label: diff --git a/jdaviz/components/child_layer_icon.vue b/jdaviz/components/child_layer_icon.vue new file mode 100644 index 0000000000..4d00657341 --- /dev/null +++ b/jdaviz/components/child_layer_icon.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/jdaviz/components/data_menu_remove.vue b/jdaviz/components/data_menu_remove.vue new file mode 100644 index 0000000000..8a8e4d24b6 --- /dev/null +++ b/jdaviz/components/data_menu_remove.vue @@ -0,0 +1,61 @@ + + + \ No newline at end of file diff --git a/jdaviz/components/data_menu_subset_edit.vue b/jdaviz/components/data_menu_subset_edit.vue new file mode 100644 index 0000000000..f93d05aa25 --- /dev/null +++ b/jdaviz/components/data_menu_subset_edit.vue @@ -0,0 +1,98 @@ + + + + + \ No newline at end of file diff --git a/jdaviz/components/plugin_live_results_icon.vue b/jdaviz/components/plugin_live_results_icon.vue new file mode 100644 index 0000000000..4b9e628beb --- /dev/null +++ b/jdaviz/components/plugin_live_results_icon.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/jdaviz/configs/default/plugins/data_menu/data_menu.py b/jdaviz/configs/default/plugins/data_menu/data_menu.py index 5c4acb930b..be6c68fd92 100644 --- a/jdaviz/configs/default/plugins/data_menu/data_menu.py +++ b/jdaviz/configs/default/plugins/data_menu/data_menu.py @@ -1,11 +1,18 @@ +import os + from contextlib import contextmanager -from traitlets import Bool, Dict, Unicode, List, observe +from traitlets import Bool, Dict, Unicode, Integer, List, observe from jdaviz.core.template_mixin import (TemplateMixin, LayerSelectMixin, DatasetSelectMixin) from jdaviz.core.user_api import UserApiWrapper from jdaviz.core.events import IconsUpdatedMessage, AddDataMessage from jdaviz.utils import cmap_samples, is_not_wcs_only +from glue.core.edit_subset_mode import (AndMode, AndNotMode, OrMode, + ReplaceMode, XorMode, NewMode) +from glue.icons import icon_path +from glue_jupyter.common.toolbar_vuetify import read_icon + __all__ = ['DataMenu'] @@ -18,6 +25,15 @@ SUBSET_NAMES = {v: k for k, v in SUBSET_TOOL_IDS.items()} +SUBSET_MODES = { + 'new': NewMode, + 'replace': ReplaceMode, + 'or': OrMode, + 'and': AndMode, + 'xor': XorMode, + 'andnot': AndNotMode, +} + class DataMenu(TemplateMixin, LayerSelectMixin, DatasetSelectMixin): """Viewer Data Menu @@ -30,7 +46,11 @@ class DataMenu(TemplateMixin, LayerSelectMixin, DatasetSelectMixin): * :meth:`set_layer_visibility` * :meth:`toggle_layer_visibility` * :meth:`create_subset` + * :meth:`modify_subset` * :meth:`add_data` + * :meth:`view_info` + * :meth:`remove_from_viewer` + * :meth:`remove_from_app` """ template_file = __file__, "data_menu.vue" @@ -47,6 +67,24 @@ class DataMenu(TemplateMixin, LayerSelectMixin, DatasetSelectMixin): dm_layer_selected = List().tag(sync=True) + selected_n_layers = Integer(0).tag(sync=True) + selected_n_data = Integer(0).tag(sync=True) + selected_n_subsets = Integer(0).tag(sync=True) + + info_enabled = Bool(False).tag(sync=True) + info_tooltip = Unicode().tag(sync=True) + + delete_enabled = Bool(False).tag(sync=True) + delete_tooltip = Unicode().tag(sync=True) + delete_viewer_tooltip = Unicode().tag(sync=True) + delete_app_enabled = Bool(False).tag(sync=True) + delete_app_tooltip = Unicode().tag(sync=True) + + subset_edit_enabled = Bool(False).tag(sync=True) + subset_edit_tooltip = Unicode().tag(sync=True) + + subset_edit_modes = List().tag(sync=True) + dev_data_menu = Bool(False).tag(sync=True) def __init__(self, viewer, *args, **kwargs): @@ -74,6 +112,12 @@ def data_not_in_viewer(data): self.viewer_icons = dict(self.app.state.viewer_icons) self.layer_icons = dict(self.app.state.layer_icons) + self.subset_edit_modes = [{'glue_name': 'replace', 'icon': read_icon(os.path.join(icon_path("glue_replace", icon_format="svg")), 'svg+xml')}, # noqa + {'glue_name': 'or', 'icon': read_icon(os.path.join(icon_path("glue_or", icon_format="svg")), 'svg+xml')}, # noqa + {'glue_name': 'and', 'icon': read_icon(os.path.join(icon_path("glue_and", icon_format="svg")), 'svg+xml')}, # noqa + {'glue_name': 'xor', 'icon': read_icon(os.path.join(icon_path("glue_xor", icon_format="svg")), 'svg+xml')}, # noqa + {'glue_name': 'andnot', 'icon': read_icon(os.path.join(icon_path("glue_andnot", icon_format="svg")), 'svg+xml')}] # noqa + # this currently assumes that toolbar.tools_data is set at init and does not change # if we ever support dynamic tool registration, this will need to be updated self.subset_tools = [{'id': k, 'img': v['img'], 'name': SUBSET_NAMES.get(k, k)} @@ -83,9 +127,14 @@ def data_not_in_viewer(data): @property def user_api(self): expose = ['layer', 'set_layer_visibility', 'toggle_layer_visibility', - 'create_subset', 'add_data'] + 'create_subset', 'modify_subset', 'add_data', 'view_info', + 'remove_from_viewer', 'remove_from_app'] return UserApiWrapper(self, expose=expose) + @property + def existing_subset_labels(self): + return [sg.label for sg in self.app.data_collection.subset_groups] + @observe('layer_items') def _update_data_not_in_viewer(self, msg): # changing the layers in the viewer needs to trigger an update to dataset_items @@ -144,13 +193,102 @@ def _dm_layer_selected_changed(self, event={}): def _update_dm_layer_selected(self, event={}): if not hasattr(self, 'layer') or not self.layer.multiselect: # pragma: no cover return - if self._during_select_sync: + if not self._during_select_sync: + with self.during_select_sync(): + # map list of strings in self.layer.selected to indices in dm_layer_selected + layer_labels = [layer['label'] for layer in self.layer_items][::-1] + self.dm_layer_selected = [layer_labels.index(label) for label in self.layer.selected + if label in layer_labels] + + if event.get('name') == 'layer_items': + # don't need to make the updates below unless the selection has been changed return - with self.during_select_sync(): - # map list of strings in self.layer.selected to indices in dm_layer_selected - layer_labels = [layer['label'] for layer in self.layer_items][::-1] - self.dm_layer_selected = [layer_labels.index(label) for label in self.layer.selected - if label in layer_labels] + + # update internal counts and tooltips + self.selected_n_layers = len(self.layer.selected) + subset_labels = self.existing_subset_labels + self.selected_n_subsets = len([lyr for lyr in self.layer.selected if lyr in subset_labels]) + self.selected_n_data = self.selected_n_layers - self.selected_n_subsets + + # user-friendly representation of selection + selected_repr = "" + if self.selected_n_data: + selected_repr += f"data ({self.selected_n_data})" + if self.selected_n_subsets: + if self.selected_n_data: + selected_repr += " and" + if self.selected_n_subsets == 1: + selected_repr += f" subset ({self.selected_n_subsets})" + else: + selected_repr += f" subsets ({self.selected_n_subsets})" + + # layer info rules + if self.selected_n_layers == 1: + if max(self.dm_layer_selected) >= len(self.layer_items): # pragma: no cover + # can happen during state transition but should immediately be followed up + # with an update + self.info_enabled = False + self.info_tooltip = '' + if self.layer_items[self.dm_layer_selected[0]].get('from_plugin', False): + self.info_enabled = False + self.info_tooltip = 'Selected data layer is a plugin product and does not have metadata' # noqa + else: + self.info_enabled = True + if self.selected_n_data == 1: + self.info_tooltip = 'View metadata for selected data' + else: + self.info_tooltip = 'View subset info for selected subset' + else: + self.info_enabled = False + if self.selected_n_layers == 0: + self.info_tooltip = 'Select a layer to view info' + else: + self.info_tooltip = 'Select a single layer to view info' + + # delete layer rules + if self.selected_n_layers == 0: + self.delete_tooltip = "Select layer(s) to delete" + self.delete_enabled = False + else: + self.delete_tooltip = f"Remove selected {selected_repr}..." + self.delete_enabled = True + + # delete from entire app rules + subset_str = "subset" if self.selected_n_subsets == 1 else "subsets" + if self.selected_n_subsets and self.selected_n_data: + self.delete_viewer_tooltip = f"Remove selected data and hide selected {subset_str} in this viewer" # noqa + elif self.selected_n_data: + self.delete_viewer_tooltip = "Remove selected data from this viewer" + elif self.selected_n_subsets: + self.delete_viewer_tooltip = f"Hide selected {subset_str} in this viewer" + + delete_app_tooltip = "Remove from all viewers and application (permanent, might affect existing subsets)" # noqa + if self.app.config == 'cubeviz': + # forbid deleting non-plugin generated data + selected_items = self.layer.selected_item + for i, layer in enumerate(self.layer.selected): + if (layer not in self.existing_subset_labels + and selected_items['from_plugin'][i] is None): + self.delete_app_enabled = False + self.delete_app_tooltip = f"Cannot delete imported data from {self.app.config}" + break + else: + self.delete_app_enabled = True + self.delete_app_tooltip = delete_app_tooltip + else: + self.delete_app_enabled = True + self.delete_app_tooltip = delete_app_tooltip + + # subset edit rules + if self.selected_n_subsets == 1 and self.selected_n_layers == 1: + self.subset_edit_enabled = True + self.subset_edit_tooltip = f"Edit {self.layer_selected[0]}..." + else: + self.subset_edit_enabled = False + if self.selected_n_subsets == 0: + self.subset_edit_tooltip = "Select a subset to edit" + else: + self.subset_edit_tooltip = "Select a single subset to edit" def set_layer_visibility(self, layer_label, visible=True): """ @@ -196,32 +334,35 @@ def toggle_layer_visibility(self, layer_label): def vue_set_layer_visibility(self, info, *args): return self.set_layer_visibility(info.get('layer'), info.get('value')) # pragma: no cover - def add_data(self, data_label): + def add_data(self, *data_labels): """ Add a dataset to the viewer. Parameters ---------- - data_label : str - The label of the dataset to add to the viewer. + *data_labels : str + The label(s) of the dataset to add to the viewer. """ - if data_label not in self.dataset.choices: - raise ValueError(f"Data label '{data_label}' not able to be loaded into '{self.viewer_id}'. Must be one of: {self.dataset.choices}") # noqa - return self.app.add_data_to_viewer(self.viewer_id, data_label) + unavailable = [data_label for data_label in data_labels + if data_label not in self.dataset.choices] + if len(unavailable): + raise ValueError(f"Data labels {unavailable} not able to be loaded into '{self.viewer_id}'. Must be one of: {self.dataset.choices}") # noqa + for data_label in data_labels: + self.app.add_data_to_viewer(self.viewer_id, data_label) def vue_add_data_to_viewer(self, info, *args): self.add_data(info.get('data_label')) # pragma: no cover def create_subset(self, subset_type): """ - Create a new subset in the viewer. This sets the app-wide subset selection to 'Create New' - and selects the appropriate tool in this viewer's toolbar. + Interactively create a new subset in the viewer. This sets the app-wide subset + selection to 'Create New' and selects the appropriate tool in this viewer's toolbar. Parameters ---------- subset_type : str The type of subset to create. Must be one of 'circle', 'rectangle', 'ellipse', - 'annulus', 'xrange', or 'yrange'. + 'annulus', 'xrange', or 'yrange', and must be an available tool in this viewer. """ # clear previous selection, finalize subsets, temporarily sets default tool self._viewer.toolbar.active_tool_id = None @@ -231,3 +372,94 @@ def create_subset(self, subset_type): def vue_create_subset(self, info, *args): self.create_subset(info.get('subset_type')) # pragma: no cover + + def modify_subset(self, combination_mode, subset_type): + """ + Interactively modify an existing subset in the viewer. This sets the app-wide subset + selection to the currently selected subset, mode to the selected combination_mode, + and selects the appropriate tool in this viewer's toolbar. + + Parameters + ---------- + combination_mode : str + The combination mode to apply to the subset. Must be one of 'replace', 'or', 'and', + 'xor', or 'andnot'. + subset_type : str + The type of subset to modify. Must be one of 'circle', 'rectangle', 'ellipse', + 'annulus', 'xrange', or 'yrange', and must be an available tool in this viewer. + """ + # future improvement: allow overriding layer.selected, with pre-validation + if len(self.layer.selected) != 1: + raise ValueError("Only one layer can be selected to modify subset.") + if self.layer.selected[0] not in self.existing_subset_labels: + raise ValueError("Selected layer is not a subset.") + subset = self.layer.selected[0] + + # set tool first since that might default to "Create New" + self._viewer.toolbar.select_tool(SUBSET_TOOL_IDS.get(subset_type, subset_type)) + # set subset selection to the subset to modify + subset_grp = [sg for sg in self.app.data_collection.subset_groups if sg.label == subset] + self.session.edit_subset_mode.edit_subset = subset_grp + # set combination mode + self.session.edit_subset_mode.mode = SUBSET_MODES.get(combination_mode) + + def vue_modify_subset(self, info, *args): + self.modify_subset(info.get('combination_mode'), + info.get('subset_type')) # pragma: no cover + + def view_info(self): + """ + View info for the selected layer by opening either the metadata or subset plugin to the + selected entry. + """ + # future improvement: allow overriding layer.selected, with pre-validation + if len(self.layer.selected) != 1: + raise ValueError("Only one layer can be selected to view info.") + if self.layer.selected[0] in self.existing_subset_labels: + sp = self._viewer.jdaviz_helper.plugins.get('Subset Tools', None) + if sp is None: # pragma: no cover + raise ValueError("subset tools plugin not available") + sp._obj.subset_select.selected = self.layer.selected[0] + sp.open_in_tray() + else: + mp = self._viewer.jdaviz_helper.plugins.get('Metadata', None) + if mp is None: # pragma: no cover + raise ValueError("metadata plugin not available") + mp.dataset.selected = self.layer.selected[0] + mp.open_in_tray() + + def vue_view_info(self, *args): + self.view_info() # pragma: no cover + + def remove_from_viewer(self): + """ + Remove the selected layers from the viewer. For subset layers, this + sets the visibility of the subset layer. For data layers, + this unloads the data from the viewer, but keeps it in the application or other viewers. + """ + # future improvement: allow overriding layer.selected via *args, with pre-validation + for layer in self.layer.selected: + if layer in self.existing_subset_labels: + self.set_layer_visibility(layer, visible=False) + else: + self.app.remove_data_from_viewer(self.viewer_id, layer) + + def vue_remove_from_viewer(self, *args): + self.remove_from_viewer() # pragma: no cover + + def remove_from_app(self): + """ + Remove the selected layers from the entire app and all viewers. + """ + # future improvement: allow overriding layer.selected via *args, with pre-validation + for layer in self.layer.selected: + if layer in self.existing_subset_labels: + for sg in self.app.data_collection.subset_groups: + if sg.label == layer: + self.app.data_collection.remove_subset_group(sg) + break + else: + self.app.vue_data_item_remove({'item_name': layer}) + + def vue_remove_from_app(self, *args): + self.remove_from_app() # pragma: no cover diff --git a/jdaviz/configs/default/plugins/data_menu/data_menu.vue b/jdaviz/configs/default/plugins/data_menu/data_menu.vue index 8de84bc32a..c1627a09c3 100644 --- a/jdaviz/configs/default/plugins/data_menu/data_menu.vue +++ b/jdaviz/configs/default/plugins/data_menu/data_menu.vue @@ -46,8 +46,10 @@ /> - - {{item.label}} + + + + {{ item.label }} @@ -109,14 +111,9 @@ - - mdi-layers-outline - - {{ item.label }} + + + {{ item.label }} @@ -135,41 +132,39 @@ + - mdi-delete - - - - mdi-label - - - Edit Subset - - + diff --git a/jdaviz/configs/default/tests/test_data_menu.py b/jdaviz/configs/default/tests/test_data_menu.py index af3105b42a..9316ac0cc7 100644 --- a/jdaviz/configs/default/tests/test_data_menu.py +++ b/jdaviz/configs/default/tests/test_data_menu.py @@ -1,3 +1,4 @@ +import pytest import numpy as np from specutils import SpectralRegion @@ -68,7 +69,7 @@ def test_data_menu_selection(specviz_helper, spectrum1d): assert dm.layer.selected == ['test'] -def test_data_menu_add_data(imviz_helper): +def test_data_menu_add_remove_data(imviz_helper): for i in range(3): imviz_helper.load_data(np.zeros((2, 2)) + i, data_label=f'image_{i}', show_in_viewer=False) @@ -81,6 +82,24 @@ def test_data_menu_add_data(imviz_helper): assert dm.layer.choices == ['image_0'] assert len(dm._obj.dataset.choices) == 2 + with pytest.raises(ValueError, + match="Data labels \\['dne1', 'dne2'\\] not able to be loaded into 'imviz-0'. Must be one of: \\['image_1', 'image_2'\\]"): # noqa + dm.add_data('dne1', 'dne2') + + dm.add_data('image_1', 'image_2') + assert len(dm.layer.choices) == 3 + assert len(dm._obj.dataset.choices) == 0 + + dm.layer.selected = ['image_0'] + dm.remove_from_viewer() + assert len(dm.layer.choices) == 2 + assert len(dm._obj.dataset.choices) == 1 + + dm.layer.selected = ['image_1'] + dm.remove_from_app() + assert len(dm.layer.choices) == 1 + assert len(dm._obj.dataset.choices) == 1 + def test_data_menu_create_subset(imviz_helper): imviz_helper.load_data(np.zeros((2, 2)), data_label='image', show_in_viewer=True) @@ -91,3 +110,84 @@ def test_data_menu_create_subset(imviz_helper): dm.create_subset('circle') assert imviz_helper.app.session.edit_subset_mode.edit_subset == [] assert imviz_helper.viewers['imviz-0']._obj.toolbar.active_tool_id == 'bqplot:truecircle' + + +def test_data_menu_remove_subset(specviz_helper, spectrum1d): + # load 2 data entries + specviz_helper.load_data(spectrum1d, data_label="test") + new_spec = specviz_helper.get_spectra(apply_slider_redshift=True)["test"]*0.9 + specviz_helper.load_data(new_spec, data_label="test2") + + dm = specviz_helper.viewers['spectrum-viewer']._obj.data_menu + sp = specviz_helper.plugins['Subset Tools'] + + sp._obj.import_region(SpectralRegion(6000 * spectrum1d.spectral_axis.unit, + 6100 * spectrum1d.spectral_axis.unit), + combination_mode='new') + sp._obj.import_region(SpectralRegion(6000 * spectrum1d.spectral_axis.unit, + 6100 * spectrum1d.spectral_axis.unit), + combination_mode='new') + + assert dm.layer.choices == ['test', 'test2', 'Subset 1', 'Subset 2'] + dm.layer.selected = ['Subset 1'] + dm.remove_from_viewer() + + # subset visibility is set to false, but still appears in menu (unlike removing data) + assert dm.layer.choices == ['test', 'test2', 'Subset 1', 'Subset 2'] + assert dm._obj.layer_items[2]['label'] == 'Subset 1' + # TODO: sometimes appearing as mixed right now, known bug + assert dm._obj.layer_items[2]['visible'] is not True + + # selection should not have changed by removing subset from viewer + assert dm.layer.selected == ['Subset 1'] + dm.remove_from_app() + # TODO: not quite sure why this isn't passing, seems to + # work on local tests, so may just be async? + # assert dm.layer.choices == ['test', 'test2', 'Subset 2'] + + +@pytest.mark.skip(reason="known issue") +def test_data_menu_subset_appearance(specviz_helper, spectrum1d): + # NOTE: this test is similar to above - the subset is appearing in time IF there + # are two data entries, but not in this case with just one + specviz_helper.load_data(spectrum1d, data_label="test") + + dm = specviz_helper.viewers['spectrum-viewer']._obj.data_menu + sp = specviz_helper.plugins['Subset Tools'] + + sp._obj.import_region(SpectralRegion(6000 * spectrum1d.spectral_axis.unit, + 6100 * spectrum1d.spectral_axis.unit)) + + assert dm.layer.choices == ['test', 'Subset 1'] + + +def test_data_menu_view_info(specviz_helper, spectrum1d): + # load 2 data entries + specviz_helper.load_data(spectrum1d, data_label="test") + new_spec = specviz_helper.get_spectra(apply_slider_redshift=True)["test"]*0.9 + specviz_helper.load_data(new_spec, data_label="test2") + + dm = specviz_helper.viewers['spectrum-viewer']._obj.data_menu + mp = specviz_helper.plugins['Metadata'] + sp = specviz_helper.plugins['Subset Tools'] + + sp._obj.import_region(SpectralRegion(6000 * spectrum1d.spectral_axis.unit, + 6100 * spectrum1d.spectral_axis.unit), + combination_mode='new') + sp._obj.import_region(SpectralRegion(6200 * spectrum1d.spectral_axis.unit, + 6300 * spectrum1d.spectral_axis.unit), + combination_mode='new') + + assert dm.layer.choices == ['test', 'test2', 'Subset 1', 'Subset 2'] + + dm.layer.selected = ["test2"] + dm.view_info() + assert mp.dataset.selected == "test2" + + dm.layer.selected = ["Subset 2"] + dm.view_info() + assert sp._obj.subset_select.selected == "Subset 2" + + dm.layer.selected = ["test", "test2"] + with pytest.raises(ValueError, match="Only one layer can be selected to view info"): + dm.view_info() diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 6e24465e06..db5466aab6 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -1538,6 +1538,8 @@ def not_trace(lyr): def _layer_to_dict(self, layer_label): is_subset = None subset_type = None + from_plugin = None + live_plugin_results = None colors = [] visibilities = [] linewidths = [] @@ -1549,6 +1551,10 @@ def _layer_to_dict(self, layer_label): (hasattr(layer, 'layer') and hasattr(layer.layer, 'subset_state'))) # noqa if is_subset: subset_type = get_subset_type(layer.layer) + if from_plugin is None: + from_plugin = layer.layer.data.meta.get('Plugin', None) + if live_plugin_results is None: + live_plugin_results = layer.layer.data.meta.get('_update_live_plugin_results', None) is not None # noqa if (getattr(viewer.state, 'color_mode', None) == 'Colormaps' and hasattr(layer.state, 'cmap')): @@ -1563,6 +1569,8 @@ def _layer_to_dict(self, layer_label): return {"label": layer_label, "is_subset": is_subset, "subset_type": subset_type, + "from_plugin": from_plugin, + "live_plugin_results": live_plugin_results, "icon": self.app.state.layer_icons.get(layer_label), "visible": visibilities[0] if len(list(set(visibilities))) == 1 else 'mixed', "linewidth": linewidths[0] if len(list(set(linewidths))) == 1 else 'mixed', @@ -1730,7 +1738,7 @@ def selected_obj(self): layers = [[layer for layer in viewer.layers if layer.layer.label in selected and self._is_valid_item(layer.layer)] - for viewer in viewers] + for viewer in viewers if viewer is not None] if not self.is_multiselect and len(layers) == 1: return layers[0]