class SubsetFacetState(State): log = CallbackProperty(False) v_min = CallbackProperty(0.) v_max = CallbackProperty(1.) steps = CallbackProperty(5) data = SelectionCallbackProperty() att = SelectionCallbackProperty() cmap = CallbackProperty() def __init__(self, data_collection): super(SubsetFacetState, self).__init__() self.data_helper = DataCollectionComboHelper(self, 'data', data_collection) self.att_helper = ComponentIDComboHelper(self, 'att') self.lim_helper = StateAttributeLimitsHelper(self, attribute='att', lower='v_min', upper='v_max', log='log') self.add_callback('data', self._on_data_change) self._on_data_change() def _on_data_change(self, *args, **kwargs): self.att_helper.set_multiple_data( [] if self.data is None else [self.data])
class SaveDataState(State): data = SelectionCallbackProperty() subset = SelectionCallbackProperty() component = SelectionCallbackProperty() exporter = SelectionCallbackProperty() def __init__(self, data_collection=None): super(SaveDataState, self).__init__() self.data_helper = DataCollectionComboHelper(self, 'data', data_collection) self.component_helper = ComponentIDComboHelper( self, 'component', data_collection=data_collection) self.add_callback('data', self._on_data_change) self._on_data_change() self._sync_data_exporters() def _sync_data_exporters(self): exporters = list(config.data_exporter) def display_func(exporter): if exporter.extension == '': return "{0} (*)".format(exporter.label) else: return "{0} ({1})".format( exporter.label, ' '.join('*.' + ext for ext in exporter.extension)) SaveDataState.exporter.set_choices(self, exporters) SaveDataState.exporter.set_display_func(self, display_func) def _on_data_change(self, event=None): self.component_helper.set_multiple_data([self.data]) self._sync_subsets() def _sync_subsets(self): def display_func(subset): if subset is None: return "All data (no subsets applied)" else: return subset.label subsets = [None] + list(self.data.subsets) SaveDataState.subset.set_choices(self, subsets) SaveDataState.subset.set_display_func(self, display_func)
class TutorialViewerState(ViewerState): x_att = SelectionCallbackProperty( docstring='The attribute to use on the x-axis') y_att = SelectionCallbackProperty( docstring='The attribute to use on the y-axis') def __init__(self, *args, **kwargs): super(TutorialViewerState, self).__init__(*args, **kwargs) self._x_att_helper = ComponentIDComboHelper(self, 'x_att') self._y_att_helper = ComponentIDComboHelper(self, 'y_att') self.add_callback('layers', self._on_layers_change) def _on_layers_change(self, value): # self.layers_data is a shortcut for # [layer_state.layer for layer_state in self.layers] self._x_att_helper.set_multiple_data(self.layers_data) self._y_att_helper.set_multiple_data(self.layers_data)
class WWTImageLayerState(LayerState): """A state object for WWT image layers """ layer = CallbackProperty() color = CallbackProperty() alpha = CallbackProperty() vmin = CallbackProperty() vmax = CallbackProperty() img_data_att = SelectionCallbackProperty(default_index=0) stretch = SelectionCallbackProperty(default_index=0, choices=VALID_STRETCHES) def __init__(self, layer=None, **kwargs): super(WWTImageLayerState, self).__init__(layer=layer) self._sync_color = keep_in_sync(self, 'color', self.layer.style, 'color') self._sync_alpha = keep_in_sync(self, 'alpha', self.layer.style, 'alpha') self.color = self.layer.style.color self.img_data_att_helper = ComponentIDComboHelper(self, 'img_data_att', numeric=True, categorical=False) self.add_callback('layer', self._on_layer_change) if layer is not None: self._on_layer_change() self.update_from_dict(kwargs) def _on_layer_change(self, layer=None): if self.layer is None: self.img_data_att_helper.set_multiple_data([]) else: self.img_data_att_helper.set_multiple_data([self.layer]) def update_priority(self, name): return 0 if name.endswith(('vmin', 'vmax')) else 1
def __new__(cls, function, data1=None, data2=None, cids1=None, cid_out=None, names1=None, names2=None, display=None, description=None): if isinstance(function, ComponentLink): names1 = function.input_names names2 = [function.output_name] elif isinstance(function, LinkCollection): names1 = function.labels1 names2 = function.labels2 elif type(function) is type and issubclass(function, LinkCollection): names1 = function.labels1 names2 = function.labels2 class CustomizedStateClass(EditableLinkFunctionState): pass if names1 is None: names1 = getfullargspec(function)[0] if names2 is None: names2 = [] setattr(CustomizedStateClass, 'names1', names1) setattr(CustomizedStateClass, 'names2', names2) for index, input_arg in enumerate(CustomizedStateClass.names1): setattr(CustomizedStateClass, input_arg, SelectionCallbackProperty(default_index=index)) for index, output_arg in enumerate(CustomizedStateClass.names2): setattr(CustomizedStateClass, output_arg, SelectionCallbackProperty(default_index=index)) return super(EditableLinkFunctionState, cls).__new__(CustomizedStateClass)
def ui_and_state(self): if isinstance(self.params, list): choices = self.params display_func = None else: params_inv = dict((value, key) for key, value in self.params.items()) choices = list(params_inv.keys()) display_func = params_inv.get property = SelectionCallbackProperty(default_index=0, choices=choices, display_func=display_func) return 'combosel_', QComboBox, property
class VolumeLayerState(VispyLayerState): """ A state object for volume layers """ attribute = SelectionCallbackProperty() vmin = CallbackProperty() vmax = CallbackProperty() subset_mode = CallbackProperty('data') limits_cache = CallbackProperty({}) def __init__(self, layer=None, **kwargs): super(VolumeLayerState, self).__init__(layer=layer) if self.layer is not None: self.color = self.layer.style.color self.alpha = self.layer.style.alpha self.att_helper = ComponentIDComboHelper(self, 'attribute') self.lim_helper = StateAttributeLimitsHelper(self, attribute='attribute', lower='vmin', upper='vmax', cache=self.limits_cache) self.add_callback('layer', self._on_layer_change) if layer is not None: self._on_layer_change() if isinstance(self.layer, Subset): self.vmin = 0 self.vmax = 1 self.update_from_dict(kwargs) def _on_layer_change(self, layer=None): with delay_callback(self, 'vmin', 'vmin'): if self.layer is None: self.att_helper.set_multiple_data([]) else: self.att_helper.set_multiple_data([self.layer]) def update_priority(self, name): return 0 if name.endswith(('vmin', 'vmax')) else 1
class TutorialViewerState(MatplotlibDataViewerState): x_att = SelectionCallbackProperty( docstring='The attribute to use on the x-axis') y_att = SelectionCallbackProperty( docstring='The attribute to use on the y-axis') def __init__(self, *args, **kwargs): super(TutorialViewerState, self).__init__(*args, **kwargs) self._x_att_helper = ComponentIDComboHelper(self, 'x_att') self._y_att_helper = ComponentIDComboHelper(self, 'y_att') self.add_callback('layers', self._on_layers_change) self.add_callback('x_att', self._on_attribute_change) self.add_callback('y_att', self._on_attribute_change) def _on_layers_change(self, value): self._x_att_helper.set_multiple_data(self.layers_data) self._y_att_helper.set_multiple_data(self.layers_data) def _on_attribute_change(self, value): if self.x_att is not None: self.x_axislabel = self.x_att.label if self.y_att is not None: self.y_axislabel = self.y_att.label
class Scatter3DLayerState(ScatterLayerState): # FIXME: the following should be a SelectionCallbackProperty geo = CallbackProperty('diamond', docstring="Type of marker") vz_att = SelectionCallbackProperty( docstring="The attribute to use for the z vector arrow") def __init__(self, viewer_state=None, layer=None, **kwargs): self.vz_att_helper = ComponentIDComboHelper(self, 'vz_att', numeric=True, categorical=False) super(Scatter3DLayerState, self).__init__(viewer_state=viewer_state, layer=layer) # self.update_from_dict(kwargs) def _on_layer_change(self, layer=None): super(Scatter3DLayerState, self)._on_layer_change(layer=layer) if self.layer is None: self.vz_att_helper.set_multiple_data([]) else: self.vz_att_helper.set_multiple_data([self.layer])
class Vispy3DViewerState(ViewerState): """ A common state object for all vispy 3D viewers """ x_att = SelectionCallbackProperty() x_min = CallbackProperty(0) x_max = CallbackProperty(1) x_stretch = CallbackProperty(1.) y_att = SelectionCallbackProperty(default_index=1) y_min = CallbackProperty(0) y_max = CallbackProperty(1) y_stretch = CallbackProperty(1.) z_att = SelectionCallbackProperty(default_index=2) z_min = CallbackProperty(0) z_max = CallbackProperty(1) z_stretch = CallbackProperty(1.) visible_axes = CallbackProperty(True) perspective_view = CallbackProperty(False) clip_data = CallbackProperty(True) native_aspect = CallbackProperty(False) line_width = CallbackProperty(1.) layers = ListCallbackProperty() limits_cache = CallbackProperty() def _update_priority(self, name): if name == 'layers': return 2 elif name.endswith(('_min', '_max')): return 0 else: return 1 def __init__(self, **kwargs): super(Vispy3DViewerState, self).__init__(**kwargs) if self.limits_cache is None: self.limits_cache = {} self.x_lim_helper = StateAttributeLimitsHelper(self, attribute='x_att', lower='x_min', upper='x_max', cache=self.limits_cache) self.y_lim_helper = StateAttributeLimitsHelper(self, attribute='y_att', lower='y_min', upper='y_max', cache=self.limits_cache) self.z_lim_helper = StateAttributeLimitsHelper(self, attribute='z_att', lower='z_min', upper='z_max', cache=self.limits_cache) # TODO: if limits_cache is re-assigned to a different object, we need to # update the attribute helpers. However if in future we make limits_cache # into a smart dictionary that can call callbacks when elements are # changed then we shouldn't always call this. It'd also be nice to # avoid this altogether and make it more clean. self.add_callback('limits_cache', self._update_limits_cache) def reset_limits(self): self.x_lim_helper.log = False self.x_lim_helper.percentile = 100. self.x_lim_helper.update_values(force=True) self.y_lim_helper.log = False self.y_lim_helper.percentile = 100. self.y_lim_helper.update_values(force=True) self.z_lim_helper.log = False self.z_lim_helper.percentile = 100. self.z_lim_helper.update_values(force=True) def _update_limits_cache(self, *args): self.x_lim_helper._cache = self.limits_cache self.x_lim_helper._update_attribute() self.y_lim_helper._cache = self.limits_cache self.y_lim_helper._update_attribute() self.z_lim_helper._cache = self.limits_cache self.z_lim_helper._update_attribute() @property def aspect(self): # TODO: this could be cached based on the limits, but is not urgent aspect = np.array([1, 1, 1], dtype=float) if self.native_aspect: aspect[0] = 1. aspect[1] = (self.y_max - self.y_min) / (self.x_max - self.x_min) aspect[2] = (self.z_max - self.z_min) / (self.x_max - self.x_min) aspect /= aspect.max() return aspect def reset(self): pass def flip_x(self): self.x_lim_helper.flip_limits() def flip_y(self): self.y_lim_helper.flip_limits() def flip_z(self): self.z_lim_helper.flip_limits() @property def clip_limits(self): return (self.x_min, self.x_max, self.y_min, self.y_max, self.z_min, self.z_max) def set_limits(self, x_min, x_max, y_min, y_max, z_min, z_max): with delay_callback(self, 'x_min', 'x_max', 'y_min', 'y_max', 'z_min', 'z_max'): self.x_min = x_min self.x_max = x_max self.y_min = y_min self.y_max = y_max self.z_min = z_min self.z_max = z_max
class ProfileTools(QtWidgets.QWidget): fit_function = SelectionCallbackProperty() collapse_function = SelectionCallbackProperty() def __init__(self, parent=None): super(ProfileTools, self).__init__(parent=parent) self._viewer = weakref.ref(parent) self.ui = load_ui('profile_tools.ui', self, directory=os.path.dirname(__file__)) fix_tab_widget_fontsize(self.ui.tabs) self.image_viewer = None @property def viewer(self): return self._viewer() def show(self, *args): super(ProfileTools, self).show(*args) self._on_tab_change() def hide(self, *args): super(ProfileTools, self).hide(*args) self.rng_mode.deactivate() self.nav_mode.deactivate() def enable(self): self.nav_mode = NavigateMouseMode(self.viewer, press_callback=self._on_nav_activate) self.rng_mode = RangeMouseMode(self.viewer) self.nav_mode.state.add_callback('x', self._on_slider_change) self.ui.tabs.setCurrentIndex(0) self.ui.tabs.currentChanged.connect(self._on_tab_change) self.ui.button_settings.clicked.connect(self._on_settings) self.ui.button_fit.clicked.connect(self._on_fit) self.ui.button_clear.clicked.connect(self._on_clear) self.ui.button_collapse.clicked.connect(self._on_collapse) font = QtGui.QFont("Courier") font.setStyleHint(font.Monospace) self.ui.text_log.document().setDefaultFont(font) self.ui.text_log.setLineWrapMode(self.ui.text_log.NoWrap) self.axes = self.viewer.axes self.canvas = self.axes.figure.canvas self._fit_artists = [] ProfileTools.fit_function.set_choices(self, list(fit_plugin)) ProfileTools.fit_function.set_display_func(self, lambda fitter: fitter.label) self._connection_fit = connect_combo_selection(self, 'fit_function', self.ui.combosel_fit_function) ProfileTools.collapse_function.set_choices(self, list(COLLAPSE_FUNCS)) ProfileTools.collapse_function.set_display_func(self, COLLAPSE_FUNCS.get) self._connection_collapse = connect_combo_selection(self, 'collapse_function', self.ui.combosel_collapse_function) self._toolbar_connected = False self.viewer.toolbar_added.connect(self._on_toolbar_added) self.viewer.state.add_callback('x_att', self._on_x_att_change) def _on_x_att_change(self, *event): self.nav_mode.clear() self.rng_mode.clear() def _on_nav_activate(self, *args): self._nav_data = self._visible_data() self._nav_viewers = {} for data in self._nav_data: pix_cid = is_convertible_to_single_pixel_cid(data, self.viewer.state.x_att_pixel) self._nav_viewers[data] = self._viewers_with_data_slice(data, pix_cid) def _on_slider_change(self, *args): x = self.nav_mode.state.x if x is None: return for data in self._nav_data: axis, slc = self._get_axis_and_pixel_slice(data, x) for viewer in self._nav_viewers[data]: slices = list(viewer.state.slices) slices[axis] = slc viewer.state.slices = tuple(slices) def _get_axis_and_pixel_slice(self, data, x): if self.viewer.state.x_att in data.pixel_component_ids: axis = self.viewer.state.x_att.axis slc = int(round(x)) else: pix_cid = is_convertible_to_single_pixel_cid(data, self.viewer.state.x_att_pixel) axis = pix_cid.axis axis_view = [0] * data.ndim axis_view[pix_cid.axis] = slice(None) axis_values = data[self.viewer.state.x_att, axis_view] slc = int(np.argmin(np.abs(axis_values - x))) return axis, slc def _on_settings(self): d = FitSettingsWidget(self.fit_function()) d.exec_() def _on_fit(self): """ Fit a model to the data The fitting happens on a dedicated thread, to keep the UI responsive """ if self.rng_mode.state.x_min is None or self.rng_mode.state.x_max is None: return x_range = self.rng_mode.state.x_range fitter = self.fit_function() def on_success(result): fit_results, x, y = result report = "" normalize = {} for layer_artist in fit_results: report += ("<b><font color='{0}'>{1}</font>" "</b>".format(color2hex(layer_artist.state.color), layer_artist.layer.label)) report += "<pre>" + fitter.summarize(fit_results[layer_artist], x, y) + "</pre>" if self.viewer.state.normalize: normalize[layer_artist] = layer_artist.state.normalize_values self._report_fit(report) self._plot_fit(fitter, fit_results, x, y, normalize) def on_fail(exc_info): exc = '\n'.join(traceback.format_exception(*exc_info)) self._report_fit("Error during fitting:\n%s" % exc) def on_done(): self.ui.button_fit.setText("Fit") self.ui.button_fit.setEnabled(True) self.canvas.draw_idle() self.ui.button_fit.setText("Running...") self.ui.button_fit.setEnabled(False) w = Worker(self._fit, fitter, xlim=x_range) w.result.connect(on_success) w.error.connect(on_fail) w.finished.connect(on_done) self._fit_worker = w # hold onto a reference w.start() def wait_for_fit(self): self._fit_worker.wait() def _report_fit(self, report): self.ui.text_log.document().setHtml(report) def _on_clear(self): self.ui.text_log.document().setPlainText('') self._clear_fit() self.canvas.draw_idle() def _fit(self, fitter, xlim=None): # We cycle through all the visible layers and get the plotted data # for each one of them. results = {} for layer in self.viewer.layers: if layer.enabled and layer.visible: x, y = layer.state.profile x = np.asarray(x) y = np.asarray(y) keep = (x >= min(xlim)) & (x <= max(xlim)) if len(x) > 0: results[layer] = fitter.build_and_fit(x[keep], y[keep]) return results, x, y def _clear_fit(self): for artist in self._fit_artists[:]: artist.remove() self._fit_artists.remove(artist) def _plot_fit(self, fitter, fit_result, x, y, normalize): self._clear_fit() for layer in fit_result: # y_model = fitter.predict(fit_result[layer], x) self._fit_artists.append(fitter.plot(fit_result[layer], self.axes, x, alpha=layer.state.alpha, linewidth=layer.state.linewidth * 0.5, color=layer.state.color, normalize=normalize.get(layer, None))[0]) self.canvas.draw_idle() def _visible_data(self): datasets = set() for layer_artist in self.viewer.layers: if layer_artist.enabled and layer_artist.visible: if isinstance(layer_artist.state.layer, BaseData): datasets.add(layer_artist.state.layer) elif isinstance(layer_artist.state.layer, Subset): datasets.add(layer_artist.state.layer.data) return list(datasets) def _viewers_with_data_slice(self, data, xatt): if self.viewer.session.application is None: return [] viewers = [] for tab in self.viewer.session.application.viewers: for viewer in tab: if isinstance(viewer, ImageViewer): for layer_artist in viewer._layer_artist_container[data]: if layer_artist.enabled and layer_artist.visible: if len(viewer.state.slices) >= xatt.axis: viewers.append(viewer) return viewers def _on_collapse(self): if self.rng_mode.state.x_min is None or self.rng_mode.state.x_max is None: return func = self.collapse_function x_range = self.rng_mode.state.x_range for data in self._visible_data(): pix_cid = is_convertible_to_single_pixel_cid(data, self.viewer.state.x_att_pixel) for viewer in self._viewers_with_data_slice(data, pix_cid): slices = list(viewer.state.slices) # TODO: don't need to fetch axis twice axis, imin = self._get_axis_and_pixel_slice(data, x_range[0]) axis, imax = self._get_axis_and_pixel_slice(data, x_range[1]) current_slice = slices[axis] if isinstance(current_slice, AggregateSlice): current_slice = current_slice.center imin, imax = min(imin, imax), max(imin, imax) slices[axis] = AggregateSlice(slice(imin, imax), current_slice, func) viewer.state.slices = tuple(slices) @property def mode(self): return MODES[self.tabs.currentIndex()] def _on_toolbar_added(self, *event): self.viewer.toolbar.tool_activated.connect(self._on_toolbar_activate) self.viewer.toolbar.tool_deactivated.connect(self._on_tab_change) def _on_toolbar_activate(self, *event): self.rng_mode.deactivate() self.nav_mode.deactivate() def _on_tab_change(self, *event): mode = self.mode if mode == 'navigate': self.rng_mode.deactivate() self.nav_mode.activate() else: self.rng_mode.activate() self.nav_mode.deactivate()
class WWTDataViewerState(ViewerState): mode = SelectionCallbackProperty(default_index=0) frame = SelectionCallbackProperty(default_index=0) lon_att = SelectionCallbackProperty(default_index=0) lat_att = SelectionCallbackProperty(default_index=1) alt_att = SelectionCallbackProperty(default_index=0) alt_unit = SelectionCallbackProperty(default_index=0) alt_type = SelectionCallbackProperty(default_index=0) foreground = SelectionCallbackProperty(default_index=1) foreground_opacity = CallbackProperty(1) background = SelectionCallbackProperty(default_index=8) galactic = CallbackProperty(False) layers = ListCallbackProperty() # For now we need to include this here otherwise when loading files, the # imagery layers are only available asynchronously and the session loading # fails. imagery_layers = ListCallbackProperty() def __init__(self, **kwargs): super(WWTDataViewerState, self).__init__() WWTDataViewerState.mode.set_choices(self, ['Sky'] + MODES_3D + MODES_BODIES) WWTDataViewerState.frame.set_choices(self, CELESTIAL_FRAMES) WWTDataViewerState.alt_unit.set_choices(self, [str(x) for x in ALT_UNITS]) WWTDataViewerState.alt_type.set_choices(self, ALT_TYPES) self.add_callback('imagery_layers', self._update_imagery_layers) self.lon_att_helper = ComponentIDComboHelper(self, 'lon_att', numeric=True, categorical=False, world_coord=True, pixel_coord=False) self.lat_att_helper = ComponentIDComboHelper(self, 'lat_att', numeric=True, categorical=False, world_coord=True, pixel_coord=False) self.alt_att_helper = ComponentIDComboHelper(self, 'alt_att', numeric=True, categorical=False, world_coord=True, pixel_coord=False, none='None') self.add_callback('layers', self._on_layers_changed) self._on_layers_changed() self.update_from_dict(kwargs) def _on_layers_changed(self, *args): self.lon_att_helper.set_multiple_data(self.layers_data) self.lat_att_helper.set_multiple_data(self.layers_data) self.alt_att_helper.set_multiple_data(self.layers_data) def _update_imagery_layers(self, *args): WWTDataViewerState.foreground.set_choices(self, self.imagery_layers) WWTDataViewerState.background.set_choices(self, self.imagery_layers) def _update_priority(self, name): if name == 'layers': return 2 elif name == 'imagery_layers': return 1 else: return 0
class ScatterLayerState(VispyLayerState): """ A state object for volume layers """ size_mode = CallbackProperty('Fixed') size = CallbackProperty() size_attribute = SelectionCallbackProperty() size_vmin = CallbackProperty() size_vmax = CallbackProperty() size_scaling = CallbackProperty(1) color_mode = CallbackProperty('Fixed') cmap_attribute = SelectionCallbackProperty() cmap_vmin = CallbackProperty() cmap_vmax = CallbackProperty() cmap = CallbackProperty() xerr_visible = CallbackProperty(False) xerr_attribute = SelectionCallbackProperty() yerr_visible = CallbackProperty(False) yerr_attribute = SelectionCallbackProperty() zerr_visible = CallbackProperty(False) zerr_attribute = SelectionCallbackProperty() vector_visible = CallbackProperty(False) vx_attribute = SelectionCallbackProperty() vy_attribute = SelectionCallbackProperty() vz_attribute = SelectionCallbackProperty() vector_scaling = CallbackProperty(1) vector_origin = SelectionCallbackProperty(default_index=1) vector_arrowhead = CallbackProperty() size_limits_cache = CallbackProperty({}) cmap_limits_cache = CallbackProperty({}) def __init__(self, layer=None, **kwargs): self._sync_markersize = None super(ScatterLayerState, self).__init__(layer=layer) if self.layer is not None: self.color = self.layer.style.color self.size = self.layer.style.markersize self.alpha = self.layer.style.alpha self.size_att_helper = ComponentIDComboHelper(self, 'size_attribute') self.cmap_att_helper = ComponentIDComboHelper(self, 'cmap_attribute') self.xerr_att_helper = ComponentIDComboHelper(self, 'xerr_attribute', categorical=False) self.yerr_att_helper = ComponentIDComboHelper(self, 'yerr_attribute', categorical=False) self.zerr_att_helper = ComponentIDComboHelper(self, 'zerr_attribute', categorical=False) self.vx_att_helper = ComponentIDComboHelper(self, 'vx_attribute', categorical=False) self.vy_att_helper = ComponentIDComboHelper(self, 'vy_attribute', categorical=False) self.vz_att_helper = ComponentIDComboHelper(self, 'vz_attribute', categorical=False) self.size_lim_helper = StateAttributeLimitsHelper( self, attribute='size_attribute', lower='size_vmin', upper='size_vmax', cache=self.size_limits_cache) self.cmap_lim_helper = StateAttributeLimitsHelper( self, attribute='cmap_attribute', lower='cmap_vmin', upper='cmap_vmax', cache=self.cmap_limits_cache) vector_origin_display = { 'tail': 'Tail of vector', 'middle': 'Middle of vector', 'tip': 'Tip of vector' } ScatterLayerState.vector_origin.set_choices(self, ['tail', 'middle', 'tip']) ScatterLayerState.vector_origin.set_display_func( self, vector_origin_display.get) self.add_callback('layer', self._on_layer_change) if layer is not None: self._on_layer_change() self.cmap = colormaps.members[0][1] self.update_from_dict(kwargs) def _on_layer_change(self, layer=None): with delay_callback(self, 'cmap_vmin', 'cmap_vmax', 'size_vmin', 'size_vmax'): helpers = [ self.size_att_helper, self.cmap_att_helper, self.xerr_att_helper, self.yerr_att_helper, self.zerr_att_helper, self.vx_att_helper, self.vy_att_helper, self.vz_att_helper ] if self.layer is None: for helper in helpers: helper.set_multiple_data([]) else: for helper in helpers: helper.set_multiple_data([self.layer]) def update_priority(self, name): return 0 if name.endswith(('vmin', 'vmax')) else 1 def _layer_changed(self): super(ScatterLayerState, self)._layer_changed() if self._sync_markersize is not None: self._sync_markersize.stop_syncing() if self.layer is not None: self.size = self.layer.style.markersize self._sync_markersize = keep_in_sync(self, 'size', self.layer.style, 'markersize') def flip_size(self): self.size_lim_helper.flip_limits() def flip_cmap(self): self.cmap_lim_helper.flip_limits()
class ViewerState3D(ViewerState): """ A common state object for all 3D viewers """ x_att = SelectionCallbackProperty() x_min = CallbackProperty(0) x_max = CallbackProperty(1) # x_stretch = CallbackProperty(1.) y_att = SelectionCallbackProperty(default_index=1) y_min = CallbackProperty(0) y_max = CallbackProperty(1) # y_stretch = CallbackProperty(1.) z_att = SelectionCallbackProperty(default_index=2) z_min = CallbackProperty(0) z_max = CallbackProperty(1) # z_stretch = CallbackProperty(1.) visible_axes = CallbackProperty(True) # perspective_view = CallbackProperty(False) # clip_data = CallbackProperty(False) # native_aspect = CallbackProperty(False) limits_cache = CallbackProperty() # def _update_priority(self, name): # if name == 'layers': # return 2 # elif name.endswith(('_min', '_max')): # return 0 # else: # return 1 def __init__(self, **kwargs): super(ViewerState3D, self).__init__(**kwargs) if self.limits_cache is None: self.limits_cache = {} self.x_lim_helper = StateAttributeLimitsHelper(self, attribute='x_att', lower='x_min', upper='x_max', cache=self.limits_cache) self.y_lim_helper = StateAttributeLimitsHelper(self, attribute='y_att', lower='y_min', upper='y_max', cache=self.limits_cache) self.z_lim_helper = StateAttributeLimitsHelper(self, attribute='z_att', lower='z_min', upper='z_max', cache=self.limits_cache) # TODO: if limits_cache is re-assigned to a different object, we need to # update the attribute helpers. However if in future we make limits_cache # into a smart dictionary that can call callbacks when elements are # changed then we shouldn't always call this. It'd also be nice to # avoid this altogether and make it more clean. self.add_callback('limits_cache', nonpartial(self._update_limits_cache)) def _update_limits_cache(self): self.x_lim_helper._cache = self.limits_cache self.x_lim_helper._update_attribute() self.y_lim_helper._cache = self.limits_cache self.y_lim_helper._update_attribute() self.z_lim_helper._cache = self.limits_cache self.z_lim_helper._update_attribute() # @property # def aspect(self): # # TODO: this could be cached based on the limits, but is not urgent # aspect = np.array([1, 1, 1], dtype=float) # if self.native_aspect: # aspect[0] = 1. # aspect[1] = (self.y_max - self.y_min) / (self.x_max - self.x_min) # aspect[2] = (self.z_max - self.z_min) / (self.x_max - self.x_min) # aspect /= aspect.max() # return aspect # def reset(self): # pass def flip_x(self): self.x_lim_helper.flip_limits() def flip_y(self): self.y_lim_helper.flip_limits() def flip_z(self): self.z_lim_helper.flip_limits()
class WWTTableLayerState(LayerState): """ A state object for WWT layers """ layer = CallbackProperty() color = CallbackProperty() size = CallbackProperty() alpha = CallbackProperty() size_mode = SelectionCallbackProperty(default_index=0) size = CallbackProperty() size_att = SelectionCallbackProperty() size_vmin = CallbackProperty() size_vmax = CallbackProperty() size_scaling = CallbackProperty(1) color_mode = SelectionCallbackProperty(default_index=0) cmap_att = SelectionCallbackProperty() cmap_vmin = CallbackProperty() cmap_vmax = CallbackProperty() cmap = CallbackProperty() cmap_mode = color_mode size_limits_cache = CallbackProperty({}) cmap_limits_cache = CallbackProperty({}) img_data_att = SelectionCallbackProperty(default_index=0) def __init__(self, layer=None, **kwargs): self._sync_markersize = None super(WWTTableLayerState, self).__init__(layer=layer) self._sync_color = keep_in_sync(self, 'color', self.layer.style, 'color') self._sync_alpha = keep_in_sync(self, 'alpha', self.layer.style, 'alpha') self._sync_size = keep_in_sync(self, 'size', self.layer.style, 'markersize') self.color = self.layer.style.color self.size = self.layer.style.markersize self.alpha = self.layer.style.alpha self.size_att_helper = ComponentIDComboHelper(self, 'size_att', numeric=True, categorical=False) self.cmap_att_helper = ComponentIDComboHelper(self, 'cmap_att', numeric=True, categorical=False) self.img_data_att_helper = ComponentIDComboHelper(self, 'img_data_att', numeric=True, categorical=False) self.size_lim_helper = StateAttributeLimitsHelper(self, attribute='size_att', lower='size_vmin', upper='size_vmax', cache=self.size_limits_cache) self.cmap_lim_helper = StateAttributeLimitsHelper(self, attribute='cmap_att', lower='cmap_vmin', upper='cmap_vmax', cache=self.cmap_limits_cache) self.add_callback('layer', self._on_layer_change) if layer is not None: self._on_layer_change() self.cmap = colormaps.members[0][1] # Color and size encoding depending on attributes is only available # in PyWWT 0.6 or later. if PYWWT_LT_06: modes = ['Fixed'] else: modes = ['Fixed', 'Linear'] WWTTableLayerState.color_mode.set_choices(self, modes) WWTTableLayerState.size_mode.set_choices(self, modes) self.update_from_dict(kwargs) def _on_layer_change(self, layer=None): with delay_callback(self, 'cmap_vmin', 'cmap_vmax', 'size_vmin', 'size_vmax'): if self.layer is None: self.cmap_att_helper.set_multiple_data([]) self.size_att_helper.set_multiple_data([]) self.img_data_att_helper.set_multiple_data([]) else: self.cmap_att_helper.set_multiple_data([self.layer]) self.size_att_helper.set_multiple_data([self.layer]) self.img_data_att_helper.set_multiple_data([self.layer]) def update_priority(self, name): return 0 if name.endswith(('vmin', 'vmax')) else 1 def _layer_changed(self): super(WWTTableLayerState, self)._layer_changed() if self._sync_markersize is not None: self._sync_markersize.stop_syncing() if self.layer is not None: self.size = self.layer.style.markersize self._sync_markersize = keep_in_sync(self, 'size', self.layer.style, 'markersize') def flip_size(self): self.size_lim_helper.flip_limits() def flip_cmap(self): self.cmap_lim_helper.flip_limits()
class LinkEditorState(State): data1 = SelectionCallbackProperty() data2 = SelectionCallbackProperty() att1 = SelectionCallbackProperty() att2 = SelectionCallbackProperty() current_link = SelectionCallbackProperty() link_type = SelectionCallbackProperty() restrict_to_suggested = CallbackProperty(False) def __init__(self, data_collection, suggested_links=None): super(LinkEditorState, self).__init__() # Find identity function for func in link_function: if func.function.__name__ == 'identity': self._identity = func break else: raise ValueError("Could not find identity link function") self.data1_helper = DataCollectionComboHelper(self, 'data1', data_collection) self.data2_helper = DataCollectionComboHelper(self, 'data2', data_collection) self.att1_helper = ComponentIDComboHelper(self, 'att1', pixel_coord=True, world_coord=True) self.att2_helper = ComponentIDComboHelper(self, 'att2', pixel_coord=True, world_coord=True) # FIXME: We unregister the combo helpers straight away to avoid issues with # leftover references once the dialog is closed. This shouldn't happen # ideally so in future we should investigate how to avoid it. self.data1_helper.unregister(data_collection.hub) self.data2_helper.unregister(data_collection.hub) self.data_collection = data_collection # Convert links to editable states links = [ EditableLinkFunctionState(link) for link in data_collection.external_links ] # If supplied, also add suggested links and make sure we toggle the # suggestion flag on the link state so that we can handle suggestions # differently in the link viewer. if suggested_links is not None: for link in suggested_links: link_state = EditableLinkFunctionState(link) link_state.suggested = True links.append(link_state) self.links = links if len(data_collection) == 2: self.data1, self.data2 = self.data_collection else: self.data1 = self.data2 = None self._on_data_change() self._on_data1_change() self._on_data2_change() self.add_callback('data1', self._on_data1_change) self.add_callback('data2', self._on_data2_change) self.add_callback('restrict_to_suggested', self._on_data_change) LinkEditorState.current_link.set_display_func(self, self._display_link) @property def visible_links(self): if self.data1 is None or self.data2 is None: return [] links = [] for link in self.links: if link.suggested or not self.restrict_to_suggested: if ((link.data1 is self.data1 and link.data2 is self.data2) or (link.data1 is self.data2 and link.data2 is self.data1)): links.append(link) return links def flip_data(self, *args): # FIXME: since the links will be the same in the list of current links, # we can make sure we reselect the same one as before - it would be # better if this didn't change in the first place though. _original_current_link = self.current_link with delay_callback(self, 'data1', 'data2'): self.data1, self.data2 = self.data2, self.data1 self.current_link = _original_current_link def _on_data1_change(self, *args): if self.data1 is self.data2 and self.data1 is not None: self.data2 = next(data for data in self.data_collection if data is not self.data1) else: self._on_data_change() self.att1_helper.set_multiple_data( [] if self.data1 is None else [self.data1]) def _on_data2_change(self, *args): if self.data2 is self.data1 and self.data2 is not None: self.data1 = next(data for data in self.data_collection if data is not self.data2) else: self._on_data_change() self.att2_helper.set_multiple_data( [] if self.data2 is None else [self.data2]) def _on_data_change(self, *args): links = self.visible_links with delay_callback(self, 'current_link'): LinkEditorState.current_link.set_choices(self, links) if len(links) > 0: self.current_link = links[0] def _display_link(self, link): if link.suggested: return str(link) + ' [Suggested]' else: return str(link) def simple_link(self, *args): self.new_link(self._identity) self.current_link.x = self.att1 self.current_link.y = self.att2 def new_link(self, function_or_helper): if hasattr(function_or_helper, 'function'): link = EditableLinkFunctionState( function_or_helper.function, data1=self.data1, data2=self.data2, names2=function_or_helper.output_labels, description=function_or_helper.info, display=function_or_helper.function.__name__) elif function_or_helper.helper.cid_independent: # This shortcut is needed for e.g. the WCS auto-linker, which has a dynamic # description but doesn't need to take any component IDs. link = EditableLinkFunctionState( function_or_helper.helper(data1=self.data1, data2=self.data2)) else: link = EditableLinkFunctionState(function_or_helper.helper, data1=self.data1, data2=self.data2) self.links.append(link) with delay_callback(self, 'current_link'): self._on_data_change() self.current_link = link def remove_link(self): self.links.remove(self.current_link) self._on_data_change() def update_links_in_collection(self): links = [link_state.link for link_state in self.links] self.data_collection.set_links(links)
class ArithmeticEditorWidget(QtWidgets.QDialog): data = SelectionCallbackProperty() def __init__(self, data_collection=None, initial_data=None, parent=None): super(ArithmeticEditorWidget, self).__init__(parent=parent) self.ui = load_ui('component_arithmetic.ui', self, directory=os.path.dirname(__file__)) self.list = self.ui.list_derived_components self.data_collection = data_collection self._components_derived = defaultdict(list) self._components_other = defaultdict(list) self._state = defaultdict(dict) for data in data_collection: # First find all derived components (only ones based on arithmetic # expressions) self._components_derived[data] = [] for cid in data.derived_components: comp = data.get_component(cid) if isinstance(comp.link, ParsedComponentLink): comp_state = {} comp_state['cid'] = cid comp_state['label'] = cid.label comp_state['equation'] = comp.link._parsed self._state[data][cid] = comp_state self._components_derived[data].append(cid) # Keep track of all other components self._components_other[data] = [] for cid in data.components: if cid not in self._components_derived[data]: self._components_other[data].append(cid) # Populate data combo ArithmeticEditorWidget.data.set_choices(self, list(self.data_collection)) ArithmeticEditorWidget.data.set_display_func(self, lambda x: x.label) self._connection = connect_combo_selection(self, 'data', self.ui.combosel_data) if initial_data is None: self.ui.combosel_data.setCurrentIndex(0) else: self.data = initial_data self.ui.combosel_data.currentIndexChanged.connect( self._update_component_lists) self._update_component_lists() self.ui.button_add_derived.clicked.connect(self._add_derived_component) self.ui.button_edit_derived.clicked.connect( self._edit_derived_component) self.ui.button_remove_derived.clicked.connect( self._remove_derived_component) self.ui.list_derived_components.itemSelectionChanged.connect( self._update_selection_derived) self._update_selection_derived() self.ui.list_derived_components.itemChanged.connect(self._update_state) self.ui.list_derived_components.order_changed.connect( self._update_state) self.ui.list_derived_components.itemDoubleClicked.connect( self._edit_derived_component) self.ui.button_ok.clicked.connect(self.accept) self.ui.button_cancel.clicked.connect(self.reject) def _update_selection_derived(self): enabled = self.list.selected_cid is not None self.button_edit_derived.setEnabled(enabled) self.button_remove_derived.setEnabled(enabled) def _update_component_lists(self, *args): # This gets called when the data is changed and we need to update the # components shown in the lists. self.list.blockSignals(True) mapping = {} for cid in self.data.components: mapping[cid] = cid.label self.list.clear() for cid in self._components_derived[self.data]: label = self._state[self.data][cid]['label'] if self._state[self.data][cid]['equation'] is None: expression = '' else: expression = self._state[self.data][cid]['equation'].render( mapping) self.list.add_cid_and_label(cid, [label, expression], editable=False) self.list.blockSignals(False) self._validate() def _validate(self): # Construct a list of all labels for the current dataset so that # we can check which ones are duplicates labels = [c.label for c in self._components_other[self.data]] labels.extend([c['label'] for c in self._state[self.data].values()]) if len(labels) == 0: return label_count = Counter(labels) # It's possible that the duplicates are entirely for components not # shown in this editor, so we keep track here of whether an invalid # component has been found. invalid = False if label_count.most_common(1)[0][1] > 1: # If we are here, there are duplicates somewhere in the list # of components. brush_red = QtGui.QBrush(Qt.red) brush_black = QtGui.QBrush(Qt.black) self.list.blockSignals(True) for item in self.list: label = item.text(0) if label_count[label] > 1: item.setForeground(0, brush_red) invalid = True else: item.setForeground(0, brush_black) self.list.blockSignals(False) if invalid: self.ui.label_status.setStyleSheet('color: red') self.ui.label_status.setText( 'Error: some components have duplicate names') self.ui.button_ok.setEnabled(False) self.ui.combosel_data.setEnabled(False) else: self.ui.label_status.setStyleSheet('') self.ui.label_status.setText('') self.ui.button_ok.setEnabled(True) self.ui.combosel_data.setEnabled(True) def _update_state(self, *args): self._components_derived[self.data] = [] for item in self.list: cid = item.data(0, Qt.UserRole) self._state[self.data][cid]['label'] = item.text(0) self._components_derived[self.data].append(cid) self._update_component_lists() def _remove_derived_component(self, *args): cid = self.list.selected_cid if cid is not None: self._components_derived[self.data].remove(cid) self._state[self.data].pop(cid) self._update_component_lists() def _add_derived_component(self, *args): comp_state = {} comp_state['cid'] = ComponentID('') comp_state['label'] = '' comp_state['equation'] = None self._components_derived[self.data].append(comp_state['cid']) self._state[self.data][comp_state['cid']] = comp_state self._update_component_lists() self.list.select_cid(comp_state['cid']) result = self._edit_derived_component() if not result: # user cancelled self._components_derived[self.data].remove(comp_state['cid']) self._state[self.data].pop(comp_state['cid']) self._update_component_lists() def _edit_derived_component(self, event=None): derived_item = self.list.selected_item if derived_item is None: return False derived_cid = self.list.selected_cid # Note, we put the pixel/world components last as it's most likely the # user wants to use one of the main components. mapping = {} references = {} for cid in (self.data.main_components + self.data.pixel_component_ids + self.data.world_component_ids): if cid is not derived_cid: mapping[cid] = cid.label references[cid.label] = cid label = self._state[self.data][derived_cid]['label'] if self._state[self.data][derived_cid]['equation'] is None: equation = None else: equation = self._state[self.data][derived_cid]['equation'].render( mapping) dialog = EquationEditorDialog(label=label, equation=equation, references=references, parent=self) dialog.setWindowFlags(self.windowFlags() | Qt.Window) dialog.setFocus() dialog.raise_() dialog.exec_() if dialog.final_expression is None: return False name, equation = dialog.get_final_label_and_parsed_command() self._state[self.data][derived_cid]['label'] = name self._state[self.data][derived_cid]['equation'] = equation derived_item.setText(0, name) # Make sure we update the component list here since the equation may # have changed and we need to update the preview self._update_component_lists() return True def accept(self): for data in self._components_derived: cids_derived = self._components_derived[data] cids_other = self._components_other[data] cids_all = cids_other + cids_derived cids_existing = data.components components = dict((cid.uuid, cid) for cid in data.components) # First deal with renaming of components for cid_new in cids_derived: label = self._state[data][cid_new]['label'] if label != cid_new.label: cid_new.label = label # Second deal with the removal of components for cid_old in cids_existing: if not any(cid_old is cid_new for cid_new in cids_all): data.remove_component(cid_old) # Third, update/add arithmetic expressions as needed for cid_new in cids_derived: if any(cid_new is cid_old for cid_old in cids_existing): comp = data.get_component(cid_new) if comp.link._parsed._cmd != self._state[data][cid_new][ 'equation']._cmd: comp.link._parsed._cmd = self._state[data][cid_new][ 'equation']._cmd comp.link._parsed._references = components if data.hub: msg = NumericalDataChangedMessage(data) data.hub.broadcast(msg) else: pc = ParsedCommand( self._state[data][cid_new]['equation']._cmd, components) link = ParsedComponentLink(cid_new, pc) data.add_component_link(link) # Findally, reorder components as needed data.reorder_components(cids_all) super(ArithmeticEditorWidget, self).accept()
class ComponentManagerWidget(QtWidgets.QDialog): data = SelectionCallbackProperty() def __init__(self, data_collection=None, initial_data=None, parent=None): super(ComponentManagerWidget, self).__init__(parent=parent) self.ui = load_ui('component_manager.ui', self, directory=os.path.dirname(__file__)) self.list = {} self.list = self.ui.list_main_components self.data_collection = data_collection self._components_main = defaultdict(list) self._components_other = defaultdict(list) self._state = defaultdict(dict) for data in data_collection: for cid in data.main_components: comp_state = {} comp_state['cid'] = cid comp_state['label'] = cid.label self._state[data][cid] = comp_state self._components_main[data].append(cid) # Keep track of all other components self._components_other[data] = [] for cid in data.components: if cid not in self._components_main[data]: self._components_other[data].append(cid) # Populate data combo ComponentManagerWidget.data.set_choices(self, list(self.data_collection)) ComponentManagerWidget.data.set_display_func(self, lambda x: x.label) connect_combo_selection(self, 'data', self.ui.combosel_data) if initial_data is None: self.ui.combosel_data.setCurrentIndex(0) else: self.data = initial_data self.ui.combosel_data.currentIndexChanged.connect( self._update_component_lists) self._update_component_lists() self.ui.button_remove_main.clicked.connect(self._remove_main_component) self.ui.list_main_components.itemSelectionChanged.connect( self._update_selection_main) self._update_selection_main() self.ui.list_main_components.itemChanged.connect(self._update_state) self.ui.list_main_components.order_changed.connect(self._update_state) self.ui.button_ok.clicked.connect(self.accept) self.ui.button_cancel.clicked.connect(self.reject) def _update_selection_main(self): enabled = self.list.selected_cid is not None self.button_remove_main.setEnabled(enabled) def _update_component_lists(self, *args): # This gets called when the data is changed and we need to update the # components shown in the lists. self.list.blockSignals(True) self.list.clear() for cid in self._components_main[self.data]: self.list.add_cid_and_label(cid, [self._state[self.data][cid]['label']]) self.list.blockSignals(False) self._validate() def _validate(self): # Construct a list of all labels for the current dataset so that # we can check which ones are duplicates labels = [c.label for c in self._components_other[self.data]] labels.extend([c['label'] for c in self._state[self.data].values()]) if len(labels) == 0: return label_count = Counter(labels) # It's possible that the duplicates are entirely for components not # shown in this editor, so we keep track here of whether an invalid # component has been found. invalid = False if label_count.most_common(1)[0][1] > 1: # If we are here, there are duplicates somewhere in the list # of components. brush_red = QtGui.QBrush(Qt.red) brush_black = QtGui.QBrush(Qt.black) self.list.blockSignals(True) for item in self.list: label = item.text(0) if label_count[label] > 1: item.setForeground(0, brush_red) invalid = True else: item.setForeground(0, brush_black) self.list.blockSignals(False) if invalid: self.ui.label_status.setStyleSheet('color: red') self.ui.label_status.setText( 'Error: some components have duplicate names') self.ui.button_ok.setEnabled(False) self.ui.combosel_data.setEnabled(False) else: self.ui.label_status.setStyleSheet('') self.ui.label_status.setText('') self.ui.button_ok.setEnabled(True) self.ui.combosel_data.setEnabled(True) def _update_state(self, *args): self._components_main[self.data] = [] for item in self.list: cid = item.data(0, Qt.UserRole) self._state[self.data][cid]['label'] = item.text(0) self._components_main[self.data].append(cid) self._update_component_lists() def _remove_main_component(self, *args): cid = self.list.selected_cid if cid is not None: self._components_main[self.data].remove(cid) self._state[self.data].pop(cid) self._update_component_lists() def accept(self): for data in self._components_main: cids_main = self._components_main[data] cids_existing = data.components cids_all = data.pixel_component_ids + data.world_component_ids + cids_main + data.derived_components # First deal with renaming of components for cid_new in cids_main: label = self._state[data][cid_new]['label'] if label != cid_new.label: cid_new.label = label # Second deal with the removal of components for cid_old in cids_existing: if not any(cid_old is cid_new for cid_new in cids_all): data.remove_component(cid_old) # Findally, reorder components as needed data.reorder_components(cids_all) super(ComponentManagerWidget, self).accept()
class EquationEditorDialog(QtWidgets.QDialog): tip_text = ( "<b>Note:</b> Attribute names in the expression should be surrounded " "by {{ }} brackets (e.g. {{{example}}}), and you can use " "Numpy functions using np.<function>, as well as any " "other function defined in your config.py file.<br><br>" "<b>Example expressions:</b><br><br>" " - Subtract 10 from '{example}': {{{example}}} - 10<br>" " - Scale '{example}' to [0:1]: ({{{example}}} - np.min({{{example}}})) / np.ptp({{{example}}})<br>" " - Multiply '{example}' by pi: {{{example}}} * np.pi<br>" " - Use masking: {{{example}}} * ({{{example}}} < 1)<br>") placeholder_text = ("Type any mathematical expression here - " "you can include attribute names from the " "drop-down below by selecting them and " "clicking 'Insert'. See below for examples " "of valid expressions") attribute = SelectionCallbackProperty() def __init__(self, label=None, data=None, equation=None, references=None, parent=None): super(EquationEditorDialog, self).__init__(parent=parent) self.ui = load_ui('equation_editor.ui', self, directory=os.path.dirname(__file__)) # Get mapping from label to component ID if references is not None: self.references = references elif data is not None: self.references = OrderedDict() for cid in data.coordinate_components + data.main_components: self.references[cid.label] = cid example = sorted(self.references, key=len)[0] self.ui.text_label.setPlaceholderText("New attribute name") self.ui.expression.setPlaceholderText( self.placeholder_text.format(example=example)) self.ui.label.setText(self.tip_text.format(example=example)) if label is not None: self.ui.text_label.setText(label) self.ui.text_label.textChanged.connect(self._update_status) # Populate component combo EquationEditorDialog.attribute.set_choices(self, list(self.references)) self._connection = connect_combo_selection(self, 'attribute', self.ui.combosel_component) # Set up labels for auto-completion labels = ['{' + l + '}' for l in self.references] self.ui.expression.set_word_list(labels) if equation is not None: self.ui.expression.insertPlainText(equation) self.ui.button_ok.clicked.connect(self.accept) self.ui.button_cancel.clicked.connect(self.reject) self.ui.button_insert.clicked.connect(self._insert_component) self.ui.expression.updated.connect(self._update_status) self._update_status() def _insert_component(self): label = self.attribute self.expression.insertPlainText('{' + label + '}') def _update_status(self): # If the text hasn't changed, no need to check again if hasattr(self, '_cache') and self._cache == (self.ui.text_label.text(), self._get_raw_command()): return if self.ui.text_label.text() == "": self.ui.label_status.setStyleSheet('color: red') self.ui.label_status.setText("Attribute name not set") self.ui.button_ok.setEnabled(False) elif self._get_raw_command() == "": self.ui.label_status.setText("") self.ui.button_ok.setEnabled(False) else: try: pc = self._get_parsed_command() pc.evaluate_test() except SyntaxError: self.ui.label_status.setStyleSheet('color: red') self.ui.label_status.setText("Incomplete or invalid syntax") self.ui.button_ok.setEnabled(False) except InvalidTagError as exc: self.ui.label_status.setStyleSheet('color: red') self.ui.label_status.setText("Invalid component: {0}".format( exc.tag)) self.ui.button_ok.setEnabled(False) except Exception as exc: self.ui.label_status.setStyleSheet('color: red') self.ui.label_status.setText(str(exc)) self.ui.button_ok.setEnabled(False) else: self.ui.label_status.setStyleSheet('color: green') self.ui.label_status.setText("Valid expression") self.ui.button_ok.setEnabled(True) self._cache = self.ui.text_label.text(), self._get_raw_command() def _get_raw_command(self): return str(self.ui.expression.toPlainText()) def _get_parsed_command(self): expression = self._get_raw_command() return ParsedCommand(expression, self.references) def get_final_label_and_parsed_command(self): return self.ui.text_label.text(), self._get_parsed_command() def accept(self): self.final_expression = self._get_parsed_command()._cmd super(EquationEditorDialog, self).accept() def reject(self): self.final_expression = None super(EquationEditorDialog, self).reject()
class DummyState(State): """Mock state class for testing only.""" x_att = SelectionCallbackProperty(docstring='x test attribute') y_att = SelectionCallbackProperty(docstring='y test attribute', default_index=-1)