# Combo helpers independent of GUI framework - these operate on
# SelectionCallbackProperty objects.
import weakref
from glue.core import BaseData, Subset
from glue.core.hub import HubListener
from glue.core.message import (DataReorderComponentMessage,
ComponentsChangedMessage,
DataCollectionAddMessage,
DataCollectionDeleteMessage,
DataUpdateMessage,
DataRenameComponentMessage)
from echo import delay_callback, ChoiceSeparator
__all__ = ['ComponentIDComboHelper', 'ManualDataComboHelper',
'DataCollectionComboHelper', 'ComboHelper', 'BaseDataComboHelper']
def unique_data_iter(datasets):
"""
Return a list with only Data objects, with duplicates removed, but
preserving the original order.
"""
datasets_new = []
for dataset in datasets:
if isinstance(dataset, BaseData):
if dataset not in datasets_new:
datasets_new.append(dataset)
else:
if dataset.data not in datasets_new:
datasets_new.append(dataset.data)
return datasets_new
[docs]class ComboHelper(HubListener):
"""
Base class for any combo helper represented by a SelectionCallbackProperty.
This stores the state and selection property and exposes the ``state``,
``selection`` and ``choices`` properties.
Parameters
----------
state : :class:`~glue.core.state_objects.State`
The state to which the selection property belongs
selection_property : :class:`~echo.SelectionCallbackProperty`
The selection property representing the combo.
"""
def __init__(self, state, selection_property):
self._state = weakref.ref(state)
self.selection_property = selection_property
[docs] @property
def state(self):
"""
The state to which the selection property belongs.
"""
return self._state()
@property
def selection(self):
"""
The current selected value.
"""
return getattr(self.state, self.selection_property)
[docs] @selection.setter
def selection(self, selection):
return setattr(self.state, self.selection_property, selection)
@property
def choices(self):
"""
The current valid choices for the combo.
"""
prop = getattr(type(self.state), self.selection_property)
return prop.get_choices(self.state)
[docs] @choices.setter
def choices(self, choices):
with delay_callback(self.state, self.selection_property):
prop = getattr(type(self.state), self.selection_property)
prop.set_choices(self.state, choices)
@property
def display(self):
"""
The current display function for the combo (the function that relates
the Python objects to the display label)
"""
prop = getattr(type(self.state), self.selection_property)
return prop.get_display_func(self.state)
[docs] @display.setter
def display(self, display):
prop = getattr(type(self.state), self.selection_property)
return prop.set_display_func(self.state, display)
def _on_rename(self, msg):
# If a component ID is renamed, we don't need to refresh because the
# list of actual component IDs is the same as before. However, we do
# need to trigger a refresh of any GUI combos that use this, so we
# make the property notify a change. However, if we are inside a
# delay_callback block, the property will not be enabled, and notify()
# won't have any effect, in which case we set the 'force_next_sync'
# option which means that when exiting from the delay_callback block,
# this property will show up as having changed
prop = getattr(type(self.state), self.selection_property)
if prop.enabled(self.state):
prop.notify(self.state, self.selection, self.selection)
else:
prop.force_next_sync(self.state)
[docs]class ComponentIDComboHelper(ComboHelper):
"""
The purpose of this class is to set up a combo (represented by a
SelectionCallbackProperty) showing componentIDs for one or more datasets, and to
update these componentIDs if needed, for example if new components are added
to a dataset, or if componentIDs are renamed. This is a GUI
framework-independent implementation.
Parameters
----------
state : :class:`~glue.core.state_objects.State`
The state to which the selection property belongs
selection_property : :class:`~echo.SelectionCallbackProperty`
The selection property representing the combo.
data_collection : :class:`~glue.core.data_collection.DataCollection`, optional
The data collection to which the datasets belong - if specified,
this is used to remove datasets from the combo when they are removed
from the data collection.
data : :class:`~glue.core.data.Data`, optional
If specified, set up the combo for this dataset only and don't allow
datasets to be added/removed
numeric : `bool`, optional
Show numeric components
datetime : `bool`, optional
Show datetime components
categorical : `bool`, optional
Show categorical components
pixel_coord : `bool`, optional
Show pixel coordinate components
world_coord : `bool`, optional
Show world coordinate components
derived : `bool`, optional
Show derived components
none : `bool` or `str`, optional
Add an entry that means `None`. If a string, this is the display string
that will be shown for the `None` entry, otherwise an empty string is
shown.
"""
def __init__(self, state, selection_property,
data_collection=None, data=None,
numeric=True, datetime=True, categorical=True,
pixel_coord=False, world_coord=False, derived=True, none=False):
super(ComponentIDComboHelper, self).__init__(state, selection_property)
if isinstance(none, str):
self._none = True
self._none_label = none
else:
self._none = none
self._none_label = ''
def display_func_label(cid):
if cid is None:
return self._none_label
else:
return cid.label
self.display = display_func_label
self._numeric = numeric
self._datetime = datetime
self._categorical = categorical
self._pixel_coord = pixel_coord
self._world_coord = world_coord
self._derived = derived
if data is None:
self._manual_data = False
self._data = []
else:
self._manual_data = True
self._data = [data]
self._data_collection = data_collection
if data_collection is None:
self.hub = None
else:
if data_collection.hub is None:
raise ValueError("Hub on data collection is not set")
else:
self.hub = data_collection.hub
if data is not None:
self.refresh()
[docs] def clear(self):
self._data.clear()
self.refresh()
@property
def numeric(self):
return self._numeric
[docs] @numeric.setter
def numeric(self, value):
self._numeric = value
self.refresh()
@property
def datetime(self):
return self._datetime
[docs] @datetime.setter
def datetime(self, value):
self._datetime = value
self.refresh()
@property
def categorical(self):
return self._categorical
[docs] @categorical.setter
def categorical(self, value):
self._categorical = value
self.refresh()
@property
def pixel_coord(self):
return self._pixel_coord
[docs] @pixel_coord.setter
def pixel_coord(self, value):
self._pixel_coord = value
self.refresh()
@property
def world_coord(self):
return self._world_coord
[docs] @world_coord.setter
def world_coord(self, value):
self._world_coord = value
self.refresh()
@property
def derived(self):
return self._derived
[docs] @derived.setter
def derived(self, value):
self._derived = value
self.refresh()
@property
def none(self):
return self._none
[docs] @none.setter
def none(self, value):
if isinstance(value, str):
self._none = True
self._none_label = value
else:
self._none = value
self._none_label = ''
self.refresh()
[docs] def append_data(self, data, refresh=True):
if self._manual_data:
raise Exception("Cannot change data in ComponentIDComboHelper "
"initialized from a single dataset")
if isinstance(data, Subset):
data = data.data
if self.hub is None:
if data.hub is not None:
self.hub = data.hub
elif data.hub is not self.hub:
raise ValueError("Data Hub is different from current hub")
if data not in self._data:
self._data.append(data)
if refresh:
self.refresh()
[docs] def remove_data(self, data):
if self._manual_data:
raise Exception("Cannot change data in ComponentIDComboHelper "
"initialized from a single dataset")
if data in self._data:
self._data.remove(data)
self.refresh()
[docs] def set_multiple_data(self, datasets):
"""
Add multiple datasets to the combo in one go (and clear any previous datasets).
Parameters
----------
datasets : `list`
The list of :class:`~glue.core.data.Data` objects to add
"""
if self._manual_data:
raise Exception("Cannot change data in ComponentIDComboHelper "
"initialized from a single dataset")
self._data.clear()
for data in unique_data_iter(datasets):
self.append_data(data, refresh=False)
self.refresh()
@property
def hub(self):
return self._hub
[docs] @hub.setter
def hub(self, value):
self._hub = value
if value is not None:
self.register_to_hub(value)
[docs] def refresh(self, *args):
choices = []
if self._none:
choices.append(None)
for data in self._data:
derived_components = [cid for cid in data.derived_components if cid.parent is data]
if len(self._data) > 1:
if data.label is None or data.label == '':
choices.append(ChoiceSeparator('Untitled Data'))
else:
choices.append(ChoiceSeparator(data.label))
cids = [ChoiceSeparator('Main components')]
for cid in data.main_components:
if ((data.get_kind(cid) == 'numerical' and self.numeric) or
(data.get_kind(cid) == 'datetime' and self.datetime) or
(data.get_kind(cid) == 'categorical' and self.categorical)):
cids.append(cid)
if len(cids) > 1:
if self.pixel_coord or self.world_coord or (self.derived and len(derived_components) > 0):
choices += cids
else:
choices += cids[1:]
if self.numeric and self.derived:
cids = [ChoiceSeparator('Derived components')]
for cid in derived_components:
cids.append(cid)
if len(cids) > 1:
choices += cids
if self.pixel_coord or self.world_coord:
cids = [ChoiceSeparator('Coordinate components')]
if self.pixel_coord:
cids += data.pixel_component_ids
if self.world_coord:
cids += data.world_component_ids
if len(cids) > 1:
choices += cids
self.choices = choices
def _filter_msg(self, msg):
return msg.sender in self._data
[docs] def register_to_hub(self, hub):
hub.subscribe(self, DataRenameComponentMessage,
handler=self._on_rename,
filter=self._filter_msg)
hub.subscribe(self, DataReorderComponentMessage,
handler=self.refresh,
filter=self._filter_msg)
hub.subscribe(self, ComponentsChangedMessage,
handler=self.refresh,
filter=self._filter_msg)
if self._data_collection is not None:
hub.subscribe(self, DataCollectionDeleteMessage,
handler=self._remove_data)
def _remove_data(self, msg):
self.remove_data(msg.data)
[docs] def unregister(self, hub):
hub.unsubscribe_all(self)
[docs]class BaseDataComboHelper(ComboHelper):
"""
This is a base class for helpers for combo boxes that need to show a list
of data objects.
Parameters
----------
state : :class:`~glue.core.state_objects.State`
The state to which the selection property belongs
selection_property : :class:`~echo.SelectionCallbackProperty`
The selection property representing the combo.
data_collection : :class:`~glue.core.data_collection.DataCollection`
The data collection to which the datasets belong - this is needed
because if a dataset is removed from the data collection, we want to
remove it here.
"""
def __init__(self, state, selection_property, data_collection=None):
super(BaseDataComboHelper, self).__init__(state, selection_property)
def display_func_label(cid):
return cid.label
self.display = display_func_label
self._component_id_helpers = []
self.state.add_callback(self.selection_property, self.refresh_component_ids)
self._data_collection = data_collection
if data_collection is not None:
if data_collection.hub is None:
raise ValueError("Hub on data collection is not set")
else:
self.hub = data_collection.hub
else:
self.hub = None
[docs] def refresh(self, *args):
self.choices = [data for data in self._datasets]
self.refresh_component_ids()
[docs] def refresh_component_ids(self, *args):
data = getattr(self.state, self.selection_property)
for helper in self._component_id_helpers:
helper.clear()
if data is not None:
helper.append_data(data)
helper.refresh()
[docs] def add_component_id_combo(self, combo):
helper = ComponentIDComboHelper(combo)
self._component_id_helpers.append_data(helper)
if self._data is not None:
helper.append_data(self._data)
@property
def hub(self):
return self._hub
[docs] @hub.setter
def hub(self, value):
self._hub = value
if value is not None:
self.register_to_hub(value)
[docs] def register_to_hub(self, hub):
pass
def _on_data_update(self, msg):
if msg.attribute == 'label':
self._on_rename(msg)
else:
self.refresh()
[docs]class ManualDataComboHelper(BaseDataComboHelper):
"""
This is a helper for combo boxes that need to show a list of data objects
that is manually curated.
Datasets are added and removed using the
:meth:`~ManualDataComboHelper.append_data` and
:meth:`~ManualDataComboHelper.remove_data` methods.
Parameters
----------
state : :class:`~glue.core.state_objects.State`
The state to which the selection property belongs
selection_property : :class:`~echo.SelectionCallbackProperty`
The selection property representing the combo.
data_collection : :class:`~glue.core.data_collection.DataCollection`
The data collection to which the datasets belong - this is needed
because if a dataset is removed from the data collection, we want to
remove it here.
"""
def __init__(self, state, selection_property, data_collection=None):
super(ManualDataComboHelper, self).__init__(state, selection_property,
data_collection=data_collection)
self._datasets = []
[docs] def set_multiple_data(self, datasets):
"""
Add multiple datasets to the combo in one go (and clear any previous datasets).
Parameters
----------
datasets : `list`
The list of :class:`~glue.core.data.Data` objects to add
"""
self._datasets.clear()
for data in unique_data_iter(datasets):
self.append_data(data, refresh=False)
self.refresh()
[docs] def append_data(self, data, refresh=True):
if data in self._datasets:
return
if self.hub is None and data.hub is not None:
self.hub = data.hub
self._datasets.append(data)
if refresh:
self.refresh()
[docs] def remove_data(self, data):
if data not in self._datasets:
return
self._datasets.remove(data)
self.refresh()
def _remove_data_msg(self, msg):
self.remove_data(msg.data)
def _filter_msg(self, msg):
return msg.sender in self._datasets
def _filter_msg_dc(self, msg):
return msg.sender is self._data_collection
[docs] def register_to_hub(self, hub):
super(ManualDataComboHelper, self).register_to_hub(hub)
hub.subscribe(self, DataUpdateMessage,
handler=self._on_data_update,
filter=self._filter_msg)
hub.subscribe(self, DataCollectionDeleteMessage,
handler=self._remove_data_msg,
filter=self._filter_msg_dc)
[docs]class DataCollectionComboHelper(BaseDataComboHelper):
"""
This is a helper for combo boxes that need to show a list of data objects
that is always in sync with a :class:`~glue.core.data_collection.DataCollection`.
Parameters
----------
state : :class:`~glue.core.state_objects.State`
The state to which the selection property belongs
selection_property : :class:`~echo.SelectionCallbackProperty`
The selection property representing the combo.
data_collection : :class:`~glue.core.data_collection.DataCollection`
The data collection with which to stay in sync
"""
def __init__(self, state, selection_property, data_collection):
super(DataCollectionComboHelper, self).__init__(state, selection_property,
data_collection=data_collection)
self._datasets = data_collection
self.refresh()
def _filter_msg_in(self, msg):
return msg.sender in self._datasets
def _filter_msg_is(self, msg):
return msg.sender is self._datasets
[docs] def register_to_hub(self, hub):
super(DataCollectionComboHelper, self).register_to_hub(hub)
hub.subscribe(self, DataUpdateMessage,
handler=self._on_data_update,
filter=self._filter_msg_in)
hub.subscribe(self, DataCollectionAddMessage,
handler=self.refresh,
filter=self._filter_msg_is)
hub.subscribe(self, DataCollectionDeleteMessage,
handler=self.refresh,
filter=self._filter_msg_is)