class ChoiceElement(FormElement): """ A dropdown selector to choose between a set of items Shorthand notation is a sequence of strings or a dict:: e = FormElement.auto({'a':1, 'b':2}) e = FormElement.auto(['a', 'b', 'c']) """ state = CurrentComboProperty('ui') @classmethod def recognizes(cls, params): if isinstance(params, six.string_types): return False try: return all(isinstance(p, six.string_types) for p in params) except TypeError: return False def _build_ui(self): w = QtGui.QComboBox() for p in sorted(self.params): w.addItem(p) if isinstance(self.params, list): self.params = dict((p, p) for p in self.params) w.currentIndexChanged.connect(nonpartial(self.changed)) return w def value(self, layer=None, view=None): return self.params[self.ui.currentText()]
class ScatterLayerStyleWidget(QtGui.QWidget): size = ValueProperty('ui.value_size') symbol = CurrentComboProperty('ui.combo_symbol') alpha = ValueProperty('ui.slider_alpha', value_range=(0, 1)) def __init__(self, layer_artist): super(ScatterLayerStyleWidget, self).__init__() self.ui = load_ui('layer_style_widget.ui', self, directory=os.path.dirname(__file__)) self._setup_symbol_combo() self.layer = layer_artist.layer # Set up connections self._connect_global() # Set initial values self.symbol = self.layer.style.marker self.size = self.layer.style.markersize self.ui.label_color.setColor(self.layer.style.color) self.alpha = self.layer.style.alpha def _connect_global(self): connect_current_combo(self.layer.style, 'marker', self.ui.combo_symbol) connect_value(self.layer.style, 'markersize', self.ui.value_size) connect_color(self.layer.style, 'color', self.ui.label_color) connect_value(self.layer.style, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) def _setup_symbol_combo(self): self._symbols = list(POINT_ICONS.keys()) for idx, symbol in enumerate(self._symbols): icon = symbol_icon(symbol) self.ui.combo_symbol.addItem(icon, '', userData=symbol) self.ui.combo_symbol.setIconSize(QtCore.QSize(20, 20)) self.ui.combo_symbol.setMinimumSize(10, 32)
class ComponenentElement(FormElement, core.hub.HubListener): """ A dropdown selector to choose a component The shorthand notation is 'att':: e = FormElement.auto('att') """ _component = CurrentComboProperty('ui') @property def state(self): return self._component @state.setter def state(self, value): self._update_components() if value is None: return self._component = value @classmethod def recognizes(cls, params): return params == 'att' def _build_ui(self): result = QtGui.QComboBox() result.currentIndexChanged.connect(nonpartial(self.changed)) return result def value(self, layer=None, view=None): cid = self._component if layer is None or cid is None: return AttributeInfo.make(cid, [], None) return AttributeInfo.from_layer(layer, cid, view) def _list_components(self): """ Determine which components to list. This can be overridden by subclassing to limit which components are visible to the user. """ comps = list(set([c for l in self.container.layers for c in l.data.components if not c._hidden])) comps = sorted(comps, key=lambda x: x.label) return comps def _update_components(self): combo = self.ui old = self._component combo.blockSignals(True) combo.clear() comps = self._list_components() for c in comps: combo.addItem(c.label, userData=c) try: combo.setCurrentIndex(comps.index(old)) except ValueError: combo.setCurrentIndex(0) combo.blockSignals(False) def register_to_hub(self, hub): hub.subscribe(self, core.message.ComponentsChangedMessage, nonpartial(self._update_components)) def add_data(self, data): self._update_components()
class DendroWidget(DataViewer): """ An interactive dendrogram display """ LABEL = 'Dendrogram' _property_set = DataViewer._property_set + \ 'ylog height parent order'.split() ylog = ButtonProperty('ui.ylog', 'log scaling on y axis?') height = CurrentComboProperty('ui.heightCombo', 'height attribute') parent = CurrentComboProperty('ui.parentCombo', 'parent attribute') order = CurrentComboProperty('ui.orderCombo', 'layout sorter attribute') _toolbar_cls = MatplotlibViewerToolbar tools = ['Pick'] def __init__(self, session, parent=None): super(DendroWidget, self).__init__(session, parent) self.central_widget = MplWidget() self.option_widget = QtWidgets.QWidget() self.setCentralWidget(self.central_widget) self.ui = load_ui('options_widget.ui', self.option_widget, directory=os.path.dirname(__file__)) self.client = DendroClient( self._data, self.central_widget.canvas.fig, layer_artist_container=self._layer_artist_container) self._connect() self.initialize_toolbar() self.statusBar().setSizeGripEnabled(False) def _connect(self): ui = self.ui cl = self.client connect_bool_button(cl, 'ylog', ui.ylog) connect_current_combo(cl, 'parent_attr', ui.parentCombo) connect_current_combo(cl, 'height_attr', ui.heightCombo) connect_current_combo(cl, 'order_attr', ui.orderCombo) def initialize_toolbar(self): super(DendroWidget, self).initialize_toolbar() def on_move(mode): if mode._drag: self.client.apply_roi(mode.roi()) self.toolbar.tools['Pick']._move_callback = on_move def apply_roi(self, roi): self.client.apply_roi(roi) def _update_combos(self, data=None): data = data or self.client.display_data if data is None: return for combo in [ self.ui.heightCombo, self.ui.parentCombo, self.ui.orderCombo ]: combo.blockSignals(True) ids = [] idx = combo.currentIndex() old = combo.itemData(idx) if idx > 0 else None combo.clear() for cid in data.components: if cid.hidden and cid is not data.pixel_component_ids[0]: continue combo.addItem(cid.label, userData=cid) ids.append(cid) try: combo.setCurrentIndex(ids.index(old)) except ValueError: combo.setCurrentIndex(0) combo.blockSignals(False) def add_data(self, data): """Add a new data set to the widget :returns: True if the addition was expected, False otherwise """ if data in self.client: return self._update_combos(data) self.client.add_layer(data) return True def add_subset(self, subset): """Add a subset to the widget :returns: True if the addition was accepted, False otherwise """ self.add_data(subset.data) if subset.data in self.client: self.client.add_layer(subset) return True def register_to_hub(self, hub): super(DendroWidget, self).register_to_hub(hub) self.client.register_to_hub(hub) hub.subscribe(self, core.message.ComponentsChangedMessage, nonpartial(self._update_combos())) def unregister(self, hub): super(DendroWidget, self).unregister(hub) hub.unsubscribe_all(self.client) hub.unsubscribe_all(self) def options_widget(self): return self.option_widget @defer_draw def restore_layers(self, rec, context): from glue.core.callback_property import delay_callback with delay_callback(self.client, 'height_attr', 'parent_attr', 'order_attr'): self.client.restore_layers(rec, context) self._update_combos()
class ScatterLayerStyleWidget(QtWidgets.QWidget): # Size-related GUI elements size_mode = CurrentComboTextProperty('ui.combo_size_mode') size_attribute = CurrentComboProperty('ui.combo_size_attribute') size_vmin = FloatLineProperty('ui.value_size_vmin') size_vmax = FloatLineProperty('ui.value_size_vmax') size = FloatLineProperty('ui.value_fixed_size') try: size_scaling = ValueProperty('ui.slider_size_scaling', value_range=(0.1, 10), log=True) except TypeError: # Glue < 0.8 size_scaling = ValueProperty('ui.slider_size_scaling') # Color-related GUI elements color_mode = CurrentComboTextProperty('ui.combo_color_mode') cmap_attribute = CurrentComboProperty('ui.combo_cmap_attribute') cmap_vmin = FloatLineProperty('ui.value_cmap_vmin') cmap_vmax = FloatLineProperty('ui.value_cmap_vmax') cmap = CurrentComboProperty('ui.combo_cmap') alpha = ValueProperty('ui.slider_alpha') def __init__(self, layer_artist): super(ScatterLayerStyleWidget, self).__init__() self.ui = load_ui('layer_style_widget.ui', self, directory=os.path.dirname(__file__)) self.layer_artist = layer_artist self.layer = layer_artist.layer # Set up size and color options self._setup_size_options() self._setup_color_options() self._connect_global() # Set initial values self.layer_artist.size = self.layer.style.markersize self.layer_artist.size_scaling = 1 self.layer_artist.size_mode = 'fixed' self.size_mode = 'Fixed' self.layer_artist.color = self.layer.style.color self.layer_artist.alpha = self.layer.style.alpha self.layer_artist.color_mode = 'fixed' self.color_mode = 'Fixed' self.ui.combo_size_attribute.setCurrentIndex(0) self.ui.combo_cmap_attribute.setCurrentIndex(0) self.ui.combo_cmap.setCurrentIndex(0) self.layer_artist.visible = True self._update_size_mode() self._update_color_mode() def _connect_global(self): connect_float_edit(self.layer.style, 'markersize', self.ui.value_fixed_size) connect_color(self.layer.style, 'color', self.ui.label_color) connect_value(self.layer.style, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) def _disconnect_global(self): # FIXME: Requires the ability to disconnect connections pass def _setup_size_options(self): # Set up attribute list label_data = [(comp.label, comp) for comp in self.visible_components] update_combobox(self.ui.combo_size_attribute, label_data) # Set up connections with layer artist connect_float_edit(self.layer_artist, 'size', self.ui.value_fixed_size) connect_current_combo(self.layer_artist, 'size_attribute', self.ui.combo_size_attribute) connect_float_edit(self.layer_artist, 'size_vmin', self.ui.value_size_vmin) connect_float_edit(self.layer_artist, 'size_vmax', self.ui.value_size_vmax) connect_value(self.layer_artist, 'size_scaling', self.ui.slider_size_scaling, value_range=(0.1, 10), log=True) # Set up internal connections self.ui.combo_size_mode.currentIndexChanged.connect(self._update_size_mode) self.ui.combo_size_attribute.currentIndexChanged.connect(self._update_size_limits) self.ui.button_flip_size.clicked.connect(self._flip_size) def _update_size_mode(self): self.layer_artist.size_mode = self.size_mode.lower() if self.size_mode == "Fixed": self.ui.size_row_2.hide() self.ui.combo_size_attribute.hide() self.ui.value_fixed_size.show() else: self.ui.value_fixed_size.hide() self.ui.combo_size_attribute.show() self.ui.size_row_2.show() def _update_size_limits(self): if not hasattr(self, '_size_limits'): self._size_limits = {} if self.size_attribute in self._size_limits: self.size_vmin, self.size_vmax = self._size_limits[self.size_attribute] else: self.size_vmin, self.size_vmax = self.default_limits(self.size_attribute) self._size_limits[self.size_attribute] = self.size_vmin, self.size_vmax def _flip_size(self): self.size_vmin, self.size_vmax = self.size_vmax, self.size_vmin def _setup_color_options(self): # Set up attribute list label_data = [(comp.label, comp) for comp in self.visible_components] update_combobox(self.ui.combo_cmap_attribute, label_data) # Set up connections with layer artist connect_color(self.layer_artist, 'color', self.ui.label_color) connect_current_combo(self.layer_artist, 'cmap_attribute', self.ui.combo_cmap_attribute) connect_float_edit(self.layer_artist, 'cmap_vmin', self.ui.value_cmap_vmin) connect_float_edit(self.layer_artist, 'cmap_vmax', self.ui.value_cmap_vmax) connect_current_combo(self.layer_artist, 'cmap', self.ui.combo_cmap) connect_value(self.layer_artist, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) # Set up internal connections self.ui.combo_color_mode.currentIndexChanged.connect(self._update_color_mode) self.ui.combo_cmap_attribute.currentIndexChanged.connect(self._update_cmap_limits) self.ui.button_flip_cmap.clicked.connect(self._flip_cmap) def _update_color_mode(self): self.layer_artist.color_mode = self.color_mode.lower() if self.color_mode == "Fixed": self.ui.color_row_2.hide() self.ui.color_row_3.hide() self.ui.combo_cmap_attribute.hide() self.ui.spacer_color_label.show() self.ui.label_color.show() else: self.ui.label_color.hide() self.ui.combo_cmap_attribute.show() self.ui.spacer_color_label.hide() self.ui.color_row_2.show() self.ui.color_row_3.show() def _update_cmap_limits(self): if not hasattr(self, '_cmap_limits'): self._cmap_limits = {} if self.cmap_attribute in self._cmap_limits: self.cmap_vmin, self.cmap_vmax = self._cmap_limits[self.cmap_attribute] else: self.cmap_vmin, self.cmap_vmax = self.default_limits(self.cmap_attribute) self._cmap_limits[self.cmap_attribute] = self.cmap_vmin, self.cmap_vmax def _flip_cmap(self): self.cmap_vmin, self.cmap_vmax = self.cmap_vmax, self.cmap_vmin def default_limits(self, attribute): # For subsets, we want to compute the limits based on the full # dataset not just the subset. if isinstance(self.layer, Subset): vmin = np.nanmin(self.layer.data[attribute]) vmax = np.nanmax(self.layer.data[attribute]) else: vmin = np.nanmin(self.layer[attribute]) vmax = np.nanmax(self.layer[attribute]) return vmin, vmax @property def visible_components(self): if isinstance(self.layer, Subset): return self.layer.data.visible_components else: return self.layer.visible_components
class FitContext(SpectrumContext): """ Mode to fit a range of a spectrum with a model fitter. Fitters are taken from user-defined fit plugins, or :class:`~glue.core.fitters.BaseFitter1D` subclasses """ error = CurrentComboProperty('ui.uncertainty_combo') fitter = CurrentComboProperty('ui.profile_combo') def _setup_grip(self): self.grip = self.main.profile.new_range_grip() def _setup_widget(self): self.ui = load_ui('spectrum_fit_panel.ui', None, directory=os.path.dirname(__file__)) self.ui.uncertainty_combo.hide() self.ui.uncertainty_label.hide() font = QtGui.QFont("Courier") font.setStyleHint(font.Monospace) self.ui.results_box.document().setDefaultFont(font) self.ui.results_box.setLineWrapMode(self.ui.results_box.NoWrap) self.widget = self.ui for fitter in list(fit_plugin): self.ui.profile_combo.addItem(fitter.label, userData=fitter()) def _edit_model_options(self): d = FitSettingsWidget(self.fitter) d.exec_() def _connect(self): self.ui.fit_button.clicked.connect(nonpartial(self.fit)) self.ui.clear_button.clicked.connect(nonpartial(self.clear)) self.ui.settings_button.clicked.connect( nonpartial(self._edit_model_options)) def fit(self): """ Fit a model to the data The fitting happens on a dedicated thread, to keep the UI responsive """ xlim = self.grip.range fitter = self.fitter def on_success(result): fit_result, _, _, _ = result self._report_fit(fitter.summarize(*result)) self.main.profile.plot_fit(fitter, fit_result) 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.fit_button.setText("Fit") self.ui.fit_button.setEnabled(True) self.canvas.draw() self.ui.fit_button.setText("Running...") self.ui.fit_button.setEnabled(False) w = Worker(self.main.profile.fit, fitter, xlim=xlim) 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 _report_fit(self, report): self.ui.results_box.document().setPlainText(report) def clear(self): self.ui.results_box.document().setPlainText('') self.main.profile.clear_fit() self.canvas.draw()
class VispyOptionsWidget(QtWidgets.QWidget): x_att = CurrentComboProperty('ui.combo_x_attribute') x_min = FloatLineProperty('ui.value_x_min') x_max = FloatLineProperty('ui.value_x_max') x_stretch = FloatLineProperty('ui.value_x_stretch') y_att = CurrentComboProperty('ui.combo_y_attribute') y_min = FloatLineProperty('ui.value_y_min') y_max = FloatLineProperty('ui.value_y_max') y_stretch = FloatLineProperty('ui.value_y_stretch') z_att = CurrentComboProperty('ui.combo_z_attribute') z_min = FloatLineProperty('ui.value_z_min') z_max = FloatLineProperty('ui.value_z_max') z_stretch = FloatLineProperty('ui.value_z_stretch') visible_box = ButtonProperty('ui.checkbox_axes') perspective_view = ButtonProperty('ui.checkbox_perspective') def __init__(self, parent=None, vispy_widget=None, data_viewer=None): super(VispyOptionsWidget, self).__init__(parent=parent) self.ui = load_ui('viewer_options.ui', self, directory=os.path.dirname(__file__)) self._vispy_widget = vispy_widget vispy_widget.options = self self._data_viewer = data_viewer self.stretch_sliders = [self.ui.slider_x_stretch, self.ui.slider_y_stretch, self.ui.slider_z_stretch] self.stretch_values = [self.ui.value_x_stretch, self.ui.value_y_stretch, self.ui.value_z_stretch] self._event_lock = False for slider, label in zip(self.stretch_sliders, self.stretch_values): slider.valueChanged.connect(partial(self._update_labels_from_sliders, label, slider)) label.editingFinished.connect(partial(self._update_sliders_from_labels, slider, label)) label.setText('1.0') label.editingFinished.emit() slider.valueChanged.connect(self._update_stretch) connect_bool_button(self._vispy_widget, 'visible_axes', self.ui.checkbox_axes) connect_bool_button(self._vispy_widget, 'perspective_view', self.ui.checkbox_perspective) if self._data_viewer is not None: self.ui.combo_x_attribute.currentIndexChanged.connect(self._data_viewer._update_attributes) self.ui.combo_y_attribute.currentIndexChanged.connect(self._data_viewer._update_attributes) self.ui.combo_z_attribute.currentIndexChanged.connect(self._data_viewer._update_attributes) self.ui.combo_x_attribute.currentIndexChanged.connect(self._update_attribute_limits) self.ui.combo_y_attribute.currentIndexChanged.connect(self._update_attribute_limits) self.ui.combo_z_attribute.currentIndexChanged.connect(self._update_attribute_limits) self.ui.value_x_min.editingFinished.connect(self._update_limits) self.ui.value_y_min.editingFinished.connect(self._update_limits) self.ui.value_z_min.editingFinished.connect(self._update_limits) self.ui.value_x_max.editingFinished.connect(self._update_limits) self.ui.value_y_max.editingFinished.connect(self._update_limits) self.ui.value_z_max.editingFinished.connect(self._update_limits) self.ui.button_flip_x.clicked.connect(self._flip_x) self.ui.button_flip_y.clicked.connect(self._flip_y) self.ui.button_flip_z.clicked.connect(self._flip_z) self.ui.reset_button.clicked.connect(self._vispy_widget._reset_view) self._components = {} self._set_attributes_enabled(False) self._set_limits_enabled(False) self._first_attributes = True def set_limits(self, x_min, x_max, y_min, y_max, z_min, z_max): self._set_limits_enabled(False) 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 self._set_limits_enabled(True) self.ui.value_x_min.editingFinished.emit() def _flip_x(self): self._set_limits_enabled(False) self.x_min, self.x_max = self.x_max, self.x_min self._set_limits_enabled(True) self.ui.value_x_min.editingFinished.emit() def _flip_y(self): self._set_limits_enabled(False) self.y_min, self.y_max = self.y_max, self.y_min self._set_limits_enabled(True) self.ui.value_y_min.editingFinished.emit() def _flip_z(self): self._set_limits_enabled(False) self.z_min, self.z_max = self.z_max, self.z_min self._set_limits_enabled(True) self.ui.value_z_min.editingFinished.emit() def _set_attributes_enabled(self, value): self.ui.combo_x_attribute.setEnabled(value) self.ui.combo_y_attribute.setEnabled(value) self.ui.combo_z_attribute.setEnabled(value) self.ui.combo_x_attribute.blockSignals(not value) self.ui.combo_y_attribute.blockSignals(not value) self.ui.combo_z_attribute.blockSignals(not value) def _set_limits_enabled(self, value): self.ui.value_x_min.setEnabled(value) self.ui.value_y_min.setEnabled(value) self.ui.value_z_min.setEnabled(value) self.ui.value_x_max.setEnabled(value) self.ui.value_y_max.setEnabled(value) self.ui.value_z_max.setEnabled(value) self.ui.value_x_min.blockSignals(not value) self.ui.value_y_min.blockSignals(not value) self.ui.value_z_min.blockSignals(not value) self.ui.value_x_max.blockSignals(not value) self.ui.value_y_max.blockSignals(not value) self.ui.value_z_max.blockSignals(not value) def _update_attributes_from_data(self, data): components = data.visible_components for component_id in components: component = data.get_component(component_id) if component.categorical: continue if self.ui.combo_x_attribute.findData(component_id) == -1: self.ui.combo_x_attribute.addItem(component_id.label, userData=component_id) if self.ui.combo_y_attribute.findData(component_id) == -1: self.ui.combo_y_attribute.addItem(component_id.label, userData=component_id) if self.ui.combo_z_attribute.findData(component_id) == -1: self.ui.combo_z_attribute.addItem(component_id.label, userData=component_id) self._components[component_id] = component if self._first_attributes: n_max = len(components) self.ui.combo_x_attribute.setCurrentIndex(0) self.ui.combo_y_attribute.setCurrentIndex(min(1, n_max - 1)) self.ui.combo_z_attribute.setCurrentIndex(min(2, n_max - 1)) self._set_attributes_enabled(True) self._first_attributes = False self._update_attribute_limits() def _update_attribute_limits(self): if not hasattr(self, '_limits'): self._limits = {} self._set_limits_enabled(False) if self.x_att not in self._limits: data = self._components[self.x_att].data self._limits[self.x_att] = np.nanmin(data), np.nanmax(data) self.x_min, self.x_max = self._limits[self.x_att] if self.y_att not in self._limits: data = self._components[self.y_att].data self._limits[self.y_att] = np.nanmin(data), np.nanmax(data) self.y_min, self.y_max = self._limits[self.y_att] if self.z_att not in self._limits: data = self._components[self.z_att].data self._limits[self.z_att] = np.nanmin(data), np.nanmax(data) self.z_min, self.z_max = self._limits[self.z_att] self._set_limits_enabled(True) self.ui.value_x_min.editingFinished.emit() def _update_limits(self): if not hasattr(self, '_limits'): self._limits = {} self._limits[self.x_att] = self.x_min, self.x_max self._limits[self.y_att] = self.y_min, self.y_max self._limits[self.z_att] = self.z_min, self.z_max self._vispy_widget._update_limits() def _update_stretch(self): self._vispy_widget._update_stretch(self.x_stretch, self.y_stretch, self.z_stretch) def _update_labels_from_sliders(self, label, slider): if self._event_lock: return # prevent infinite event loop self._event_lock = True try: label.setText("{0:6.2f}".format(10 ** (slider.value() / 1e4))) finally: self._event_lock = False def _update_sliders_from_labels(self, slider, label): if self._event_lock: return # prevent infinite event loop self._event_lock = True try: slider.setValue(1e4 * math.log10(float(label.text()))) finally: self._event_lock = False def __gluestate__(self, context): return dict(x_att=context.id(self.x_att), x_min=self.x_min, x_max=self.x_max, x_stretch=self.x_stretch, y_att=context.id(self.y_att), y_min=self.y_min, y_max=self.y_max, y_stretch=self.y_stretch, z_att=context.id(self.z_att), z_min=self.z_min, z_max=self.z_max, z_stretch=self.z_stretch, visible_box=self.visible_box, perspective_view=self.perspective_view)
class SliceWidget(QtGui.QWidget): label = TextProperty('_ui_label') slice_center = ValueProperty('_ui_slider.slider') mode = CurrentComboProperty('_ui_mode') slice_changed = QtCore.Signal(int) mode_changed = QtCore.Signal(str) def __init__(self, label='', pix2world=None, lo=0, hi=10, parent=None, aggregation=None): super(SliceWidget, self).__init__(parent) if aggregation is not None: raise NotImplemented("Aggregation option not implemented") if pix2world is not None: raise NotImplemented("Pix2world option not implemented") layout = QtGui.QVBoxLayout() layout.setContentsMargins(3, 1, 3, 1) layout.setSpacing(0) top = QtGui.QHBoxLayout() top.setContentsMargins(3, 3, 3, 3) label = QtGui.QLabel(label) top.addWidget(label) mode = QtGui.QComboBox() mode.addItem('x', 'x') mode.addItem('y', 'y') mode.addItem('slice', 'slice') mode.currentIndexChanged.connect( lambda x: self.mode_changed.emit(self.mode)) mode.currentIndexChanged.connect(self._update_mode) top.addWidget(mode) layout.addLayout(top) slider = load_ui('data_slice_widget.ui', None, directory=os.path.dirname(__file__)) slider.slider slider.button_first.setStyleSheet('border: 0px') slider.button_first.setIcon(get_icon('playback_first')) slider.button_prev.setStyleSheet('border: 0px') slider.button_prev.setIcon(get_icon('playback_prev')) slider.button_back.setStyleSheet('border: 0px') slider.button_back.setIcon(get_icon('playback_back')) slider.button_stop.setStyleSheet('border: 0px') slider.button_stop.setIcon(get_icon('playback_stop')) slider.button_forw.setStyleSheet('border: 0px') slider.button_forw.setIcon(get_icon('playback_forw')) slider.button_next.setStyleSheet('border: 0px') slider.button_next.setIcon(get_icon('playback_next')) slider.button_last.setStyleSheet('border: 0px') slider.button_last.setIcon(get_icon('playback_last')) slider.slider.setMinimum(lo) slider.slider.setMaximum(hi) slider.slider.setValue((lo + hi) / 2) slider.slider.valueChanged.connect( lambda x: self.slice_changed.emit(self.mode)) slider.slider.valueChanged.connect( lambda x: slider.label.setText(str(x))) slider.label.setMinimumWidth(50) slider.label.setText(str(slider.slider.value())) slider.label.textChanged.connect( lambda x: slider.slider.setValue(int(x))) self._play_timer = QtCore.QTimer() self._play_timer.setInterval(500) self._play_timer.timeout.connect(nonpartial(self._play_slice)) slider.button_first.clicked.connect( nonpartial(self._browse_slice, 'first')) slider.button_prev.clicked.connect( nonpartial(self._browse_slice, 'prev')) slider.button_back.clicked.connect( nonpartial(self._adjust_play, 'back')) slider.button_stop.clicked.connect( nonpartial(self._adjust_play, 'stop')) slider.button_forw.clicked.connect( nonpartial(self._adjust_play, 'forw')) slider.button_next.clicked.connect( nonpartial(self._browse_slice, 'next')) slider.button_last.clicked.connect( nonpartial(self._browse_slice, 'last')) layout.addWidget(slider) self.setLayout(layout) self._ui_label = label self._ui_slider = slider self._ui_mode = mode self._update_mode() self._frozen = False self._play_speed = 0 def _adjust_play(self, action): if action == 'stop': self._play_speed = 0 elif action == 'back': if self._play_speed > 0: self._play_speed = -1 else: self._play_speed -= 1 elif action == 'forw': if self._play_speed < 0: self._play_speed = +1 else: self._play_speed += 1 if self._play_speed == 0: self._play_timer.stop() else: self._play_timer.start() self._play_timer.setInterval(500 / abs(self._play_speed)) def _play_slice(self): if self._play_speed > 0: self._browse_slice('next', play=True) elif self._play_speed < 0: self._browse_slice('prev', play=True) def _browse_slice(self, action, play=False): imin = self._ui_slider.slider.minimum() imax = self._ui_slider.slider.maximum() value = self._ui_slider.slider.value() # If this was not called from _play_slice, we should stop the # animation. if not play: self._adjust_play('stop') if action == 'first': value = imin elif action == 'last': value = imax elif action == 'prev': value = value - 1 if value < imin: value = imax elif action == 'next': value = value + 1 if value > imax: value = imin else: raise ValueError("Action should be one of first/prev/next/last") self._ui_slider.slider.setValue(value) def _update_mode(self, *args): if self.mode != 'slice': self._ui_slider.hide() self._adjust_play('stop') else: self._ui_slider.show() def freeze(self): self.mode = 'slice' self._ui_mode.setEnabled(False) self._ui_slider.hide() self._frozen = True @property def frozen(self): return self._frozen
class ScatterWidget(DataViewer): """ An interactive scatter plot. """ LABEL = "Scatter Plot" _property_set = DataViewer._property_set + \ 'xlog ylog xflip yflip hidden xatt yatt xmin xmax ymin ymax'.split() xlog = ButtonProperty('ui.xLogCheckBox', 'log scaling on x axis?') ylog = ButtonProperty('ui.yLogCheckBox', 'log scaling on y axis?') xflip = ButtonProperty('ui.xFlipCheckBox', 'invert the x axis?') yflip = ButtonProperty('ui.yFlipCheckBox', 'invert the y axis?') xmin = FloatLineProperty('ui.xmin', 'Lower x limit of plot') xmax = FloatLineProperty('ui.xmax', 'Upper x limit of plot') ymin = FloatLineProperty('ui.ymin', 'Lower y limit of plot') ymax = FloatLineProperty('ui.ymax', 'Upper y limit of plot') hidden = ButtonProperty('ui.hidden_attributes', 'Show hidden attributes') xatt = CurrentComboProperty('ui.xAxisComboBox', 'Attribute to plot on x axis') yatt = CurrentComboProperty('ui.yAxisComboBox', 'Attribute to plot on y axis') def __init__(self, session, parent=None): super(ScatterWidget, self).__init__(session, parent) self.central_widget = MplWidget() self.option_widget = QtGui.QWidget() self.setCentralWidget(self.central_widget) self.ui = load_ui('options_widget.ui', self.option_widget, directory=os.path.dirname(__file__)) self._tweak_geometry() self.client = ScatterClient( self._data, self.central_widget.canvas.fig, layer_artist_container=self._layer_artist_container) self._connect() self.unique_fields = set() tb = self.make_toolbar() cache_axes(self.client.axes, tb) self.statusBar().setSizeGripEnabled(False) self.setFocusPolicy(Qt.StrongFocus) @staticmethod def _get_default_tools(): return [] def _tweak_geometry(self): self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) def _connect(self): ui = self.ui cl = self.client connect_bool_button(cl, 'xlog', ui.xLogCheckBox) connect_bool_button(cl, 'ylog', ui.yLogCheckBox) connect_bool_button(cl, 'xflip', ui.xFlipCheckBox) connect_bool_button(cl, 'yflip', ui.yFlipCheckBox) ui.xAxisComboBox.currentIndexChanged.connect(self.update_xatt) ui.yAxisComboBox.currentIndexChanged.connect(self.update_yatt) ui.hidden_attributes.toggled.connect(lambda x: self._update_combos()) ui.swapAxes.clicked.connect(nonpartial(self.swap_axes)) ui.snapLimits.clicked.connect(cl.snap) connect_float_edit(cl, 'xmin', ui.xmin) connect_float_edit(cl, 'xmax', ui.xmax) connect_float_edit(cl, 'ymin', ui.ymin) connect_float_edit(cl, 'ymax', ui.ymax) def make_toolbar(self): result = GlueToolbar(self.central_widget.canvas, self, name='Scatter Plot') for mode in self._mouse_modes(): result.add_mode(mode) self.addToolBar(result) return result def _mouse_modes(self): axes = self.client.axes def apply_mode(mode): return self.apply_roi(mode.roi()) rect = RectangleMode(axes, roi_callback=apply_mode) xra = HRangeMode(axes, roi_callback=apply_mode) yra = VRangeMode(axes, roi_callback=apply_mode) circ = CircleMode(axes, roi_callback=apply_mode) poly = PolyMode(axes, roi_callback=apply_mode) return [rect, xra, yra, circ, poly] @defer_draw def _update_combos(self): """ Update contents of combo boxes """ # have to be careful here, since client and/or widget # are potentially out of sync layer_ids = [] # show hidden attributes if needed if ((self.client.xatt and self.client.xatt.hidden) or (self.client.yatt and self.client.yatt.hidden)): self.hidden = True # determine which components to put in combos for l in self.client.data: if not self.client.is_layer_present(l): continue for lid in self.client.plottable_attributes( l, show_hidden=self.hidden): if lid not in layer_ids: layer_ids.append(lid) oldx = self.xatt oldy = self.yatt newx = self.client.xatt or oldx newy = self.client.yatt or oldy for combo, target in zip( [self.ui.xAxisComboBox, self.ui.yAxisComboBox], [newx, newy]): combo.blockSignals(True) combo.clear() if not layer_ids: # empty component list continue # populate for lid in layer_ids: combo.addItem(lid.label, userData=lid) idx = layer_ids.index(target) if target in layer_ids else 0 combo.setCurrentIndex(idx) combo.blockSignals(False) # ensure client and widget synced self.client.xatt = self.xatt self.client.lyatt = self.yatt @defer_draw def add_data(self, data): """Add a new data set to the widget :returns: True if the addition was expected, False otherwise """ if self.client.is_layer_present(data): return if data.size > WARN_SLOW and not self._confirm_large_data(data): return False first_layer = self.client.layer_count == 0 self.client.add_data(data) self._update_combos() if first_layer: # forces both x and y axes to be rescaled self.update_xatt(None) self.update_yatt(None) self.ui.xAxisComboBox.setCurrentIndex(0) if len(data.visible_components) > 1: self.ui.yAxisComboBox.setCurrentIndex(1) else: self.ui.yAxisComboBox.setCurrentIndex(0) self.update_window_title() return True @defer_draw def add_subset(self, subset): """Add a subset to the widget :returns: True if the addition was accepted, False otherwise """ if self.client.is_layer_present(subset): return data = subset.data if data.size > WARN_SLOW and not self._confirm_large_data(data): return False first_layer = self.client.layer_count == 0 self.client.add_layer(subset) self._update_combos() if first_layer: # forces both x and y axes to be rescaled self.update_xatt(None) self.update_yatt(None) self.ui.xAxisComboBox.setCurrentIndex(0) if len(data.visible_components) > 1: self.ui.yAxisComboBox.setCurrentIndex(1) else: self.ui.yAxisComboBox.setCurrentIndex(0) self.update_window_title() return True def register_to_hub(self, hub): super(ScatterWidget, self).register_to_hub(hub) self.client.register_to_hub(hub) hub.subscribe(self, core.message.DataUpdateMessage, nonpartial(self._sync_labels)) hub.subscribe(self, core.message.ComponentsChangedMessage, nonpartial(self._update_combos)) hub.subscribe(self, core.message.ComponentReplacedMessage, self._on_component_replace) def _on_component_replace(self, msg): # let client update its state first self.client._on_component_replace(msg) self._update_combos() def unregister(self, hub): super(ScatterWidget, self).unregister(hub) hub.unsubscribe_all(self.client) hub.unsubscribe_all(self) @defer_draw def swap_axes(self): xid = self.ui.xAxisComboBox.currentIndex() yid = self.ui.yAxisComboBox.currentIndex() xlog = self.ui.xLogCheckBox.isChecked() ylog = self.ui.yLogCheckBox.isChecked() xflip = self.ui.xFlipCheckBox.isChecked() yflip = self.ui.yFlipCheckBox.isChecked() self.ui.xAxisComboBox.setCurrentIndex(yid) self.ui.yAxisComboBox.setCurrentIndex(xid) self.ui.xLogCheckBox.setChecked(ylog) self.ui.yLogCheckBox.setChecked(xlog) self.ui.xFlipCheckBox.setChecked(yflip) self.ui.yFlipCheckBox.setChecked(xflip) @defer_draw def update_xatt(self, index): component_id = self.xatt self.client.xatt = component_id @defer_draw def update_yatt(self, index): component_id = self.yatt self.client.yatt = component_id @property def window_title(self): data = self.client.data label = ', '.join([d.label for d in data if self.client.is_visible(d)]) return label def _sync_labels(self): self.update_window_title() def options_widget(self): return self.option_widget @defer_draw def restore_layers(self, rec, context): self.client.restore_layers(rec, context) self._update_combos() # manually force client attributes to sync self.update_xatt(None) self.update_yatt(None)
class ImageWidgetBase(DataViewer): """ Widget for ImageClient This base class avoids any matplotlib-specific logic """ LABEL = "Image Viewer" _property_set = DataViewer._property_set + \ 'data attribute rgb_mode rgb_viz ratt gatt batt slice'.split() attribute = CurrentComboProperty('ui.attributeComboBox', 'Current attribute') data = CurrentComboProperty('ui.displayDataCombo', 'Current data') aspect_ratio = CurrentComboProperty('ui.aspectCombo', 'Aspect ratio for image') rgb_mode = ButtonProperty('ui.rgb', 'RGB Mode?') rgb_viz = Pointer('ui.rgb_options.rgb_visible') _layer_style_widget_cls = {ScatterLayerArtist: ScatterLayerStyleWidget} def __init__(self, session, parent=None): super(ImageWidgetBase, self).__init__(session, parent) self._setup_widgets() self.client = self.make_client() self._connect() def _setup_widgets(self): self.central_widget = self.make_central_widget() self.label_widget = QtWidgets.QLabel("", self.central_widget) self.setCentralWidget(self.central_widget) self.option_widget = QtWidgets.QWidget() self.ui = load_ui('options_widget.ui', self.option_widget, directory=os.path.dirname(__file__)) self.ui.slice = DataSlice() self.ui.slice_layout.addWidget(self.ui.slice) self._tweak_geometry() self.ui.aspectCombo.addItem("Square Pixels", userData='equal') self.ui.aspectCombo.addItem("Automatic", userData='auto') def make_client(self): """ Instantiate and return an ImageClient subclass """ raise NotImplementedError() def make_central_widget(self): """ Create and return the central widget to display the image """ raise NotImplementedError() def _tweak_geometry(self): self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self.ui.rgb_options.hide() self.statusBar().setSizeGripEnabled(False) self.setFocusPolicy(Qt.StrongFocus) @defer_draw def add_data(self, data): """ Add a new dataset to the viewer """ # overloaded from DataViewer # need to delay callbacks, otherwise might # try to set combo boxes to nonexisting items with delay_callback(self.client, 'display_data', 'display_attribute'): # If there is not already any image data set, we can't add 1-D # datasets (tables/catalogs) to the image widget yet. if data.data.ndim == 1 and self.client.display_data is None: QtWidgets.QMessageBox.information( self.window(), "Note", "Cannot create image viewer from a 1-D " "dataset. You will need to first " "create an image viewer using data " "with 2 or more dimensions, after " "which you will be able to overlay 1-D " "data as a scatter plot.", buttons=QtWidgets.QMessageBox.Ok) return r = self.client.add_layer(data) if r is not None and self.client.display_data is not None: self.add_data_to_combo(data) if self.client.can_image_data(data): self.client.display_data = data self.set_attribute_combo(self.client.display_data) return r is not None @defer_draw def add_subset(self, subset): self.client.add_scatter_layer(subset) assert subset in self.client.artists def add_data_to_combo(self, data): """ Add a data object to the combo box, if not already present """ if not self.client.can_image_data(data): return combo = self.ui.displayDataCombo try: pos = _find_combo_data(combo, data) except ValueError: combo.addItem(data.label, userData=data) @property def ratt(self): """ComponentID assigned to R channel in RGB Mode""" return self.ui.rgb_options.attributes[0] @ratt.setter def ratt(self, value): att = list(self.ui.rgb_options.attributes) att[0] = value self.ui.rgb_options.attributes = att @property def gatt(self): """ComponentID assigned to G channel in RGB Mode""" return self.ui.rgb_options.attributes[1] @gatt.setter def gatt(self, value): att = list(self.ui.rgb_options.attributes) att[1] = value self.ui.rgb_options.attributes = att @property def batt(self): """ComponentID assigned to B channel in RGB Mode""" return self.ui.rgb_options.attributes[2] @batt.setter def batt(self, value): att = list(self.ui.rgb_options.attributes) att[2] = value self.ui.rgb_options.attributes = att @property def slice(self): return self.client.slice @slice.setter def slice(self, value): self.client.slice = value def set_attribute_combo(self, data): """ Update attribute combo box to reflect components in data""" labeldata = ((f.label, f) for f in data.visible_components) update_combobox(self.ui.attributeComboBox, labeldata) def _connect(self): ui = self.ui ui.monochrome.toggled.connect(self._update_rgb_console) ui.rgb_options.colors_changed.connect(self.update_window_title) # sync client and widget slices ui.slice.slice_changed.connect( lambda: setattr(self, 'slice', self.ui.slice.slice)) update_ui_slice = lambda val: setattr(ui.slice, 'slice', val) add_callback(self.client, 'slice', update_ui_slice) add_callback(self.client, 'display_data', self.ui.slice.set_data) # sync window title to data/attribute add_callback(self.client, 'display_data', nonpartial(self._display_data_changed)) add_callback(self.client, 'display_attribute', nonpartial(self._display_attribute_changed)) add_callback(self.client, 'display_aspect', nonpartial(self.client._update_aspect)) # sync data/attribute combos with client properties connect_current_combo(self.client, 'display_data', self.ui.displayDataCombo) connect_current_combo(self.client, 'display_attribute', self.ui.attributeComboBox) connect_current_combo(self.client, 'display_aspect', self.ui.aspectCombo) def _display_data_changed(self): if self.client.display_data is None: self.ui.attributeComboBox.clear() return with self.client.artists.ignore_empty(): self.set_attribute_combo(self.client.display_data) self.client.add_layer(self.client.display_data) self.client._update_and_redraw() self.update_window_title() def _display_attribute_changed(self): if self.client.display_attribute is None: return self.client._update_and_redraw() self.update_window_title() @defer_draw def _update_rgb_console(self, is_monochrome): if is_monochrome: self.ui.rgb_options.hide() self.ui.mono_att_label.show() self.ui.attributeComboBox.show() self.client.rgb_mode(False) else: self.ui.mono_att_label.hide() self.ui.attributeComboBox.hide() self.ui.rgb_options.show() rgb = self.client.rgb_mode(True) if rgb is not None: self.ui.rgb_options.artist = rgb def register_to_hub(self, hub): super(ImageWidgetBase, self).register_to_hub(hub) self.client.register_to_hub(hub) dc_filt = lambda x: x.sender is self.client._data display_data_filter = lambda x: x.data is self.client.display_data hub.subscribe(self, core.message.DataCollectionAddMessage, handler=lambda x: self.add_data_to_combo(x.data), filter=dc_filt) hub.subscribe(self, core.message.DataCollectionDeleteMessage, handler=lambda x: self.remove_data_from_combo(x.data), filter=dc_filt) hub.subscribe(self, core.message.DataUpdateMessage, handler=lambda x: self._sync_data_labels()) hub.subscribe(self, core.message.ComponentsChangedMessage, handler=lambda x: self.set_attribute_combo(x.data), filter=display_data_filter) def unregister(self, hub): super(ImageWidgetBase, self).unregister(hub) for obj in [self, self.client]: hub.unsubscribe_all(obj) def remove_data_from_combo(self, data): """ Remove a data object from the combo box, if present """ combo = self.ui.displayDataCombo pos = combo.findText(data.label) if pos >= 0: combo.removeItem(pos) def _set_norm(self, mode): """ Use the `ContrastMouseMode` to adjust the transfer function """ # at least one of the clip/vmin pairs will be None clip_lo, clip_hi = mode.get_clip_percentile() vmin, vmax = mode.get_vmin_vmax() stretch = mode.stretch return self.client.set_norm(clip_lo=clip_lo, clip_hi=clip_hi, stretch=stretch, vmin=vmin, vmax=vmax, bias=mode.bias, contrast=mode.contrast) @property def window_title(self): if self.client.display_data is None or self.client.display_attribute is None: title = '' else: data = self.client.display_data.label a = self.client.rgb_mode() if a is None: # monochrome mode title = "%s - %s" % (self.client.display_data.label, self.client.display_attribute.label) else: r = a.r.label if a.r is not None else '' g = a.g.label if a.g is not None else '' b = a.b.label if a.b is not None else '' title = "%s Red = %s Green = %s Blue = %s" % (data, r, g, b) return title def _sync_data_combo_labels(self): combo = self.ui.displayDataCombo for i in range(combo.count()): combo.setItemText(i, combo.itemData(i).label) def _sync_data_labels(self): self.update_window_title() self._sync_data_combo_labels() def __str__(self): return "Image Widget" def _confirm_large_image(self, data): """Ask user to confirm expensive operations :rtype: bool. Whether the user wishes to continue """ warn_msg = ("WARNING: Image has %i pixels, and may render slowly." " Continue?" % data.size) title = "Contour large image?" ok = QtWidgets.QMessageBox.Ok cancel = QtWidgets.QMessageBox.Cancel buttons = ok | cancel result = QtWidgets.QMessageBox.question(self, title, warn_msg, buttons=buttons, defaultButton=cancel) return result == ok def options_widget(self): return self.option_widget @defer_draw def restore_layers(self, rec, context): with delay_callback(self.client, 'display_data', 'display_attribute'): self.client.restore_layers(rec, context) for artist in self.layers: self.add_data_to_combo(artist.layer.data) self.set_attribute_combo(self.client.display_data) self._sync_data_combo_labels() def closeEvent(self, event): # close window and all plugins super(ImageWidgetBase, self).closeEvent(event)
class IsosurfaceLayerStyleWidget(QtWidgets.QWidget): # GUI elements attribute = CurrentComboProperty('ui.combo_attribute') level = FloatLineProperty('ui.value_level') alpha = ValueProperty('ui.slider_alpha') def __init__(self, layer_artist): super(IsosurfaceLayerStyleWidget, self).__init__() self.ui = load_ui('layer_style_widget.ui', self, directory=os.path.dirname(__file__)) self.layer_artist = layer_artist self.layer = layer_artist.layer # Set up attribute and visual options self._setup_options() self._connect_global() # Set initial values self.layer_artist.color = self.layer.style.color self.layer_artist.alpha = self.layer.style.alpha with delay_callback(self.layer_artist, 'attribute'): self.attribute = self.visible_components[0] self._update_levels() self.layer_artist.visible = True def _connect_global(self): connect_color(self.layer.style, 'color', self.ui.label_color) connect_value(self.layer.style, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) def _setup_options(self): """ Set up the combo box with the list of attributes """ # Set up attribute list label_data = [(comp.label, comp) for comp in self.visible_components] update_combobox(self.ui.combo_attribute, label_data) # Set up connections with layer artist connect_current_combo(self.layer_artist, 'attribute', self.ui.combo_attribute) connect_float_edit(self.layer_artist, 'level', self.ui.value_level) connect_color(self.layer_artist, 'color', self.ui.label_color) connect_value(self.layer_artist, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) # Set up internal connections self.ui.value_level.editingFinished.connect(self._cache_levels) self.ui.combo_attribute.currentIndexChanged.connect( self._update_levels) def _update_levels(self): if isinstance(self.layer, Subset): self.level = 0.5 return if not hasattr(self, '_levels'): self._levels = {} if self.attribute in self._levels: self.level = self._levels[self.attribute] else: self.level = self.default_levels(self.attribute) self._levels[self.attribute] = self.level def _cache_levels(self): if not isinstance(self.layer, Subset) or self.layer_artist.subset_mode == 'data': self._levels[self.attribute] = self.level def default_levels(self, attribute): # For subsets, we want to compute the levels based on the full # dataset not just the subset. if isinstance(self.layer, Subset): return 0.5 else: return np.nanmedian(self.layer[attribute]) @property def visible_components(self): if isinstance(self.layer, Subset): return self.layer.data.visible_components else: return self.layer.visible_components
class SliceWidget(QtGui.QWidget): label = TextProperty('_ui_label') slice_center = ValueProperty('_ui_slider.slider') mode = CurrentComboProperty('_ui_mode') slice_changed = QtCore.Signal(int) mode_changed = QtCore.Signal(str) def __init__(self, label='', pix2world=None, lo=0, hi=10, parent=None, aggregation=None): super(SliceWidget, self).__init__(parent) if aggregation is not None: raise NotImplemented("Aggregation option not implemented") if pix2world is not None: raise NotImplemented("Pix2world option not implemented") layout = QtGui.QVBoxLayout() layout.setContentsMargins(3, 1, 3, 1) layout.setSpacing(0) top = QtGui.QHBoxLayout() top.setContentsMargins(3, 3, 3, 3) label = QtGui.QLabel(label) top.addWidget(label) mode = QtGui.QComboBox() mode.addItem('x', 'x') mode.addItem('y', 'y') mode.addItem('slice', 'slice') mode.currentIndexChanged.connect(lambda x: self.mode_changed.emit(self.mode)) mode.currentIndexChanged.connect(self._update_mode) top.addWidget(mode) layout.addLayout(top) slider = load_ui('data_slice_widget.ui', None, directory=os.path.dirname(__file__)) slider.slider slider.slider.setMinimum(lo) slider.slider.setMaximum(hi) slider.slider.setValue((lo + hi) / 2) slider.slider.valueChanged.connect(lambda x: self.slice_changed.emit(self.mode)) slider.slider.valueChanged.connect(lambda x: slider.label.setText(str(x))) slider.label.setMinimumWidth(50) slider.label.setText(str(slider.slider.value())) slider.label.textChanged.connect(lambda x: slider.slider.setValue(int(x))) slider.first.clicked.connect(nonpartial(self._browse_slice, 'first')) slider.prev.clicked.connect(nonpartial(self._browse_slice, 'prev')) slider.next.clicked.connect(nonpartial(self._browse_slice, 'next')) slider.last.clicked.connect(nonpartial(self._browse_slice, 'last')) layout.addWidget(slider) self.setLayout(layout) self._ui_label = label self._ui_slider = slider self._ui_mode = mode self._update_mode() self._frozen = False def _browse_slice(self, action): imin = self._ui_slider.slider.minimum() imax = self._ui_slider.slider.maximum() value = self._ui_slider.slider.value() if action == 'first': value = imin elif action == 'last': value = imax elif action == 'prev': value = max(value - 1, imin) elif action == 'next': value = min(value + 1, imax) else: raise ValueError("Action should be one of first/prev/next/last") self._ui_slider.slider.setValue(value) def _update_mode(self, *args): if self.mode != 'slice': self._ui_slider.hide() else: self._ui_slider.show() def freeze(self): self.mode = 'slice' self._ui_mode.setEnabled(False) self._ui_slider.hide() self._frozen = True @property def frozen(self): return self._frozen
class VolumeLayerStyleWidget(QtGui.QWidget): # GUI elements attribute = CurrentComboProperty('ui.combo_attribute') vmin = FloatLineProperty('ui.value_min') vmax = FloatLineProperty('ui.value_max') alpha = ValueProperty('ui.slider_alpha') subset_outline = ButtonProperty('ui.radio_subset_outline') subset_data = ButtonProperty('ui.radio_subset_data') def __init__(self, layer_artist): super(VolumeLayerStyleWidget, self).__init__() self.ui = load_ui('layer_style_widget.ui', self, directory=os.path.dirname(__file__)) self.layer_artist = layer_artist self.layer = layer_artist.layer # Set up attribute and visual options self._setup_options() self._connect_global() # Set initial values self.layer_artist.color = self.layer.style.color self.layer_artist.alpha = self.layer.style.alpha with delay_callback(self.layer_artist, 'attribute'): self.attribute = self.visible_components[0] self._update_limits() if isinstance(self.layer, Subset): self.ui.radio_subset_data.setChecked(True) self.layer_artist.visible = True def _connect_global(self): connect_color(self.layer.style, 'color', self.ui.label_color) connect_value(self.layer.style, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) def _setup_options(self): """ Set up the combo box with the list of attributes """ # Set up radio buttons for subset mode selection if this is a subset if isinstance(self.layer, Subset): self._radio_size = QtGui.QButtonGroup() self._radio_size.addButton(self.ui.radio_subset_outline) self._radio_size.addButton(self.ui.radio_subset_data) else: self.ui.radio_subset_outline.hide() self.ui.radio_subset_data.hide() self.ui.label_subset_mode.hide() # Set up attribute list label_data = [(comp.label, comp) for comp in self.visible_components] update_combobox(self.ui.combo_attribute, label_data) # Set up connections with layer artist connect_current_combo(self.layer_artist, 'attribute', self.ui.combo_attribute) connect_float_edit(self.layer_artist, 'vmin', self.ui.value_min) connect_float_edit(self.layer_artist, 'vmax', self.ui.value_max) connect_color(self.layer_artist, 'color', self.ui.label_color) connect_value(self.layer_artist, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) # Set up internal connections self.ui.radio_subset_outline.toggled.connect(self._update_subset_mode) self.ui.radio_subset_data.toggled.connect(self._update_subset_mode) self.ui.value_min.editingFinished.connect(self._cache_limits) self.ui.value_max.editingFinished.connect(self._cache_limits) self.ui.combo_attribute.currentIndexChanged.connect( self._update_limits) def _update_subset_mode(self): if self.ui.radio_subset_outline.isChecked(): self.layer_artist.subset_mode = 'outline' else: self.layer_artist.subset_mode = 'data' self._update_limits() def _update_limits(self): if isinstance(self.layer, Subset): if self.layer_artist.subset_mode == 'outline': self.ui.value_min.setEnabled(False) self.ui.value_max.setEnabled(False) self.vmin, self.vmax = 0, 2 return else: self.ui.value_min.setEnabled(False) self.ui.value_max.setEnabled(True) if not hasattr(self, '_limits'): self._limits = {} if self.attribute in self._limits: self.vmin, self.vmax = self._limits[self.attribute] else: self.vmin, self.vmax = self.default_limits(self.attribute) self._limits[self.attribute] = self.vmin, self.vmax def _cache_limits(self): if not isinstance(self.layer, Subset) or self.layer_artist.subset_mode == 'data': self._limits[self.attribute] = self.vmin, self.vmax def default_limits(self, attribute): # For subsets, we want to compute the limits based on the full # dataset not just the subset. if isinstance(self.layer, Subset): vmin = 0 vmax = np.nanmax(self.layer.data[attribute]) else: vmin = np.nanmin(self.layer[attribute]) vmax = np.nanmax(self.layer[attribute]) return vmin, vmax @property def visible_components(self): if isinstance(self.layer, Subset): return self.layer.data.visible_components else: return self.layer.visible_components
class SliceWidget(QtWidgets.QWidget): label = TextProperty('_ui_label') slider_label = TextProperty('_ui_slider.label') slider_unit = TextProperty('_ui_slider.text_unit') slice_center = ValueProperty('_ui_slider.slider') mode = CurrentComboProperty('_ui_mode') use_world = ButtonProperty('_ui_slider.checkbox_world') slice_changed = QtCore.Signal(int) mode_changed = QtCore.Signal(str) def __init__(self, label='', world=None, lo=0, hi=10, parent=None, aggregation=None, world_unit=None, world_warning=False): super(SliceWidget, self).__init__(parent) if aggregation is not None: raise NotImplemented("Aggregation option not implemented") self._world = np.asarray(world) self._world_warning = world_warning self._world_unit = world_unit layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(3, 1, 3, 1) layout.setSpacing(0) top = QtWidgets.QHBoxLayout() top.setContentsMargins(3, 3, 3, 3) label = QtWidgets.QLabel(label) top.addWidget(label) mode = QtWidgets.QComboBox() mode.addItem('x', 'x') mode.addItem('y', 'y') mode.addItem('slice', 'slice') mode.currentIndexChanged.connect( lambda x: self.mode_changed.emit(self.mode)) mode.currentIndexChanged.connect(self._update_mode) top.addWidget(mode) layout.addLayout(top) slider = load_ui('data_slice_widget.ui', None, directory=os.path.dirname(__file__)) self._ui_slider = slider font = slider.label_warning.font() font.setPointSize(font.pointSize() * 0.75) slider.label_warning.setFont(font) slider.button_first.setStyleSheet('border: 0px') slider.button_first.setIcon(get_icon('playback_first')) slider.button_prev.setStyleSheet('border: 0px') slider.button_prev.setIcon(get_icon('playback_prev')) slider.button_back.setStyleSheet('border: 0px') slider.button_back.setIcon(get_icon('playback_back')) slider.button_stop.setStyleSheet('border: 0px') slider.button_stop.setIcon(get_icon('playback_stop')) slider.button_forw.setStyleSheet('border: 0px') slider.button_forw.setIcon(get_icon('playback_forw')) slider.button_next.setStyleSheet('border: 0px') slider.button_next.setIcon(get_icon('playback_next')) slider.button_last.setStyleSheet('border: 0px') slider.button_last.setIcon(get_icon('playback_last')) slider.slider.setMinimum(lo) slider.slider.setMaximum(hi) slider.slider.setValue((lo + hi) / 2) slider.slider.valueChanged.connect( lambda x: self.slice_changed.emit(self.mode)) slider.slider.valueChanged.connect( nonpartial(self.set_label_from_slider)) slider.label.setMinimumWidth(80) slider.label.setText(str(slider.slider.value())) slider.label.editingFinished.connect( nonpartial(self.set_slider_from_label)) self._play_timer = QtCore.QTimer() self._play_timer.setInterval(500) self._play_timer.timeout.connect(nonpartial(self._play_slice)) slider.button_first.clicked.connect( nonpartial(self._browse_slice, 'first')) slider.button_prev.clicked.connect( nonpartial(self._browse_slice, 'prev')) slider.button_back.clicked.connect( nonpartial(self._adjust_play, 'back')) slider.button_stop.clicked.connect( nonpartial(self._adjust_play, 'stop')) slider.button_forw.clicked.connect( nonpartial(self._adjust_play, 'forw')) slider.button_next.clicked.connect( nonpartial(self._browse_slice, 'next')) slider.button_last.clicked.connect( nonpartial(self._browse_slice, 'last')) slider.checkbox_world.toggled.connect( nonpartial(self.set_label_from_slider)) if world is None: self.use_world = False slider.checkbox_world.hide() else: self.use_world = not world_warning if world_unit: self.slider_unit = world_unit else: self.slider_unit = '' layout.addWidget(slider) self.setLayout(layout) self._ui_label = label self._ui_mode = mode self._update_mode() self._frozen = False self._play_speed = 0 self.set_label_from_slider() def set_label_from_slider(self): value = self._ui_slider.slider.value() if self.use_world: text = str(self._world[value]) if self._world_warning: self._ui_slider.label_warning.show() else: self._ui_slider.label_warning.hide() self.slider_unit = self._world_unit else: text = str(value) self._ui_slider.label_warning.hide() self.slider_unit = '' self._ui_slider.label.setText(text) def set_slider_from_label(self): text = self._ui_slider.label.text() if self.use_world: # Don't want to assume world is sorted, pick closest value value = np.argmin(np.abs(self._world - float(text))) self._ui_slider.label.setText(str(self._world[value])) else: value = int(text) self._ui_slider.slider.setValue(value) def _adjust_play(self, action): if action == 'stop': self._play_speed = 0 elif action == 'back': if self._play_speed > 0: self._play_speed = -1 else: self._play_speed -= 1 elif action == 'forw': if self._play_speed < 0: self._play_speed = +1 else: self._play_speed += 1 if self._play_speed == 0: self._play_timer.stop() else: self._play_timer.start() self._play_timer.setInterval(500 / abs(self._play_speed)) def _play_slice(self): if self._play_speed > 0: self._browse_slice('next', play=True) elif self._play_speed < 0: self._browse_slice('prev', play=True) def _browse_slice(self, action, play=False): imin = self._ui_slider.slider.minimum() imax = self._ui_slider.slider.maximum() value = self._ui_slider.slider.value() # If this was not called from _play_slice, we should stop the # animation. if not play: self._adjust_play('stop') if action == 'first': value = imin elif action == 'last': value = imax elif action == 'prev': value = value - 1 if value < imin: value = imax elif action == 'next': value = value + 1 if value > imax: value = imin else: raise ValueError("Action should be one of first/prev/next/last") self._ui_slider.slider.setValue(value) def _update_mode(self, *args): if self.mode != 'slice': self._ui_slider.hide() self._adjust_play('stop') else: self._ui_slider.show() def freeze(self): self.mode = 'slice' self._ui_mode.setEnabled(False) self._ui_slider.hide() self._frozen = True @property def frozen(self): return self._frozen