class LinkEquation(QtWidgets.QWidget): """ Interactively define ComponentLinks from existing functions This widget inspects the calling signatures of helper functions, and presents the user with an interface for assigning componentIDs to the input and output arguments. It also generates ComponentLinks from this information. ComponentIDs are assigned to arguments via drag and drop. This widget is used within the LinkEditor dialog Usage:: widget = LinkEquation() """ category = SelectionCallbackProperty() function = SelectionCallbackProperty() def __init__(self, parent=None): super(LinkEquation, self).__init__(parent) # Set up mapping of function/helper name -> function/helper tuple. For the helpers, we use the 'display' name if available. self._argument_widgets = [] self._output_widget = ArgumentWidget("") # pyqt4 can't take self as second argument here # for some reason. Manually embed self._ui = load_ui('link_equation.ui', None, directory=os.path.dirname(__file__)) l = QtWidgets.QHBoxLayout() l.addWidget(self._ui) self.setLayout(l) self._ui.outputs_layout.addWidget(self._output_widget) self._populate_category_combo() self.category = 'General' self._populate_function_combo() self._connect() self._setup_editor() def set_result_visible(self, state): self._output_widget.setVisible(state) self._ui.output_label.setVisible(state) def is_helper(self): return self.function is not None and \ type(self.function).__name__ == 'LinkHelper' def is_function(self): return self.function is not None and \ type(self.function).__name__ == 'LinkFunction' @property def signature(self): """ Returns the ComponentIDs assigned to the input and output arguments :rtype: tuple of (input, output). Input is a list of ComponentIDs. output is a ComponentID """ inp = [a.component_id for a in self._argument_widgets] out = self._output_widget.component_id return inp, out @signature.setter def signature(self, inout): inp, out = inout for i, a in zip(inp, self._argument_widgets): a.component_id = i self._output_widget.component_id = out @messagebox_on_error("Failed to create links") def links(self): """ Create ComponentLinks from the state of the widget :rtype: list of ComponentLinks that can be created. If no links can be created (e.g. because of missing input), the empty list is returned """ inp, out = self.signature if self.is_function(): using = self.function.function if not all(inp) or not out: return [] link = core.component_link.ComponentLink(inp, out, using) return [link] if self.is_helper(): helper = self.function.helper if not all(inp): return [] return helper(*inp) def _update_add_enabled(self): state = True for a in self._argument_widgets: state = state and a.component_id is not None if self.is_function(): state = state and self._output_widget.component_id is not None def _connect(self): signal = self._ui.function.currentIndexChanged signal.connect(nonpartial(self._setup_editor)) signal.connect(nonpartial(self._update_add_enabled)) self._output_widget.editor.textChanged.connect(nonpartial(self._update_add_enabled)) self._ui.category.currentIndexChanged.connect(self._populate_function_combo) def clear_inputs(self): for w in self._argument_widgets: w.clear() self._output_widget.clear() def _setup_editor(self): if self.is_function(): self._setup_editor_function() elif self.is_helper(): self._setup_editor_helper() def _setup_editor_function(self): """ Prepare the widget for the active function.""" assert self.is_function() self.set_result_visible(True) func = self.function.function args = getfullargspec(func)[0] label = function_label(self.function) self._ui.info.setText(label) self._output_widget.label = self.function.output_labels[0] self._clear_input_canvas() for a in args: self._add_argument_widget(a) def _setup_editor_helper(self): """Setup the editor for the selected link helper""" assert self.is_helper() self.set_result_visible(False) label = helper_label(self.function) args = self.function.input_labels self._ui.info.setText(label) self._clear_input_canvas() for a in args: self._add_argument_widget(a) def _add_argument_widget(self, argument): """ Create and add a single argument widget to the input canvas :param arguement: The argument name (string) """ widget = ArgumentWidget(argument) widget.editor.textChanged.connect(nonpartial(self._update_add_enabled)) self._ui.inputs_layout.addWidget(widget) self._argument_widgets.append(widget) def _clear_input_canvas(self): """ Remove all widgets from the input canvas """ layout = self._ui.inputs_layout for a in self._argument_widgets: layout.removeWidget(a) a.hide() a.close() self._argument_widgets = [] def _populate_category_combo(self): f = [f for f in link_function.members if len(f.output_labels) == 1] categories = sorted(set(l.category for l in f + link_helper.members)) LinkEquation.category.set_choices(self, categories) connect_combo_selection(self, 'category', self._ui.category) def _populate_function_combo(self): """ Add name of functions to function combo box """ f = [f for f in link_function.members if len(f.output_labels) == 1] functions = [l for l in f + link_helper.members if l.category == self.category] LinkEquation.function.set_choices(self, functions) LinkEquation.function.set_display_func(self, lambda l: get_function_name(l[0])) connect_combo_selection(self, 'function', self._ui.function)
class OpenSpaceViewerState(ViewerState): mode = SelectionCallbackProperty(default_index=0) frame = SelectionCallbackProperty(default_index=0) lon_att = SelectionCallbackProperty(default_index=0) lat_att = SelectionCallbackProperty(default_index=1) lum_att = SelectionCallbackProperty(default_index=0) vel_att = SelectionCallbackProperty(default_index=1) alt_att = SelectionCallbackProperty(default_index=2) alt_unit = SelectionCallbackProperty(default_index=4) alt_type = SelectionCallbackProperty(default_index=0) layers = ListCallbackProperty() def __init__(self, **kwargs): super(OpenSpaceViewerState, self).__init__() OpenSpaceViewerState.mode.set_choices(self, MODES) OpenSpaceViewerState.frame.set_choices(self, CELESTIAL_FRAMES) OpenSpaceViewerState.alt_unit.set_choices( self, [str(x) for x in ALTERNATIVE_UNITS]) OpenSpaceViewerState.alt_type.set_choices(self, ALTERNATIVE_TYPES) 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.lum_att_helper = ComponentIDComboHelper(self, 'lum_att', numeric=True, categorical=False, world_coord=True, pixel_coord=False) self.vel_att_helper = ComponentIDComboHelper(self, 'vel_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) 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.lum_att_helper.set_multiple_data(self.layers_data) self.vel_att_helper.set_multiple_data(self.layers_data) self.alt_att_helper.set_multiple_data(self.layers_data) def _update_priority(self, name): if name == 'layers': return 2 else: return 0
class CubeVizLayout(QtWidgets.QWidget): """ The 'CubeViz' layout, with three image viewers and one spectrum viewer. """ LABEL = "CubeViz" subWindowActivated = QtCore.Signal(object) single_viewer_attribute = SelectionCallbackProperty(default_index=0) viewer1_attribute = SelectionCallbackProperty(default_index=0) viewer2_attribute = SelectionCallbackProperty(default_index=1) viewer3_attribute = SelectionCallbackProperty(default_index=2) def __init__(self, session=None, parent=None): super(CubeVizLayout, self).__init__(parent=parent) if not hasattr(session.application, '_has_cubeviz_toolbar'): cubeviz_toolbar = CubevizToolbar(application=session.application) session.application.insertToolBar(session.application._data_toolbar, cubeviz_toolbar) self.session = session self._has_data = False self._wavelengths = None self._option_buttons = [] self._data = None self.ui = load_ui('layout.ui', self, directory=os.path.dirname(__file__)) # Create the views and register to the hub. self.single_view = WidgetWrapper(CubevizImageViewer(self.session, cubeviz_layout=self), tab_widget=self) self.left_view = WidgetWrapper(CubevizImageViewer(self.session, cubeviz_layout=self), tab_widget=self) self.middle_view = WidgetWrapper(CubevizImageViewer(self.session, cubeviz_layout=self), tab_widget=self) self.right_view = WidgetWrapper(CubevizImageViewer(self.session, cubeviz_layout=self), tab_widget=self) self.specviz = WidgetWrapper(SpecVizViewer(self.session), tab_widget=self) self.single_view._widget.register_to_hub(self.session.hub) self.left_view._widget.register_to_hub(self.session.hub) self.middle_view._widget.register_to_hub(self.session.hub) self.right_view._widget.register_to_hub(self.session.hub) self.specviz._widget.register_to_hub(self.session.hub) self.all_views = [self.single_view, self.left_view, self.middle_view, self.right_view] # TODO: determine whether to rename this or get rid of it self.cube_views = self.all_views self.split_views = self.cube_views[1:] self._synced_checkboxes = [ self.ui.singleviewer_synced_checkbox, self.ui.viewer1_synced_checkbox, self.ui.viewer2_synced_checkbox, self.ui.viewer3_synced_checkbox ] for view, checkbox in zip(self.all_views, self._synced_checkboxes): view._widget.assign_synced_checkbox(checkbox) # Add the views to the layouts. self.ui.single_image_layout.addWidget(self.single_view) self.ui.image_row_layout.addWidget(self.left_view) self.ui.image_row_layout.addWidget(self.middle_view) self.ui.image_row_layout.addWidget(self.right_view) self.ui.specviz_layout.addWidget(self.specviz) self.subWindowActivated.connect(self._update_active_view) self.ui.sync_button.clicked.connect(self._on_sync_click) self.ui.button_toggle_image_mode.clicked.connect( self._toggle_image_mode) # This is a list of helpers for the viewer combo boxes. New data # collections should be added to each helper in this list using the # ``append_data`` method to ensure that the new data components are # populated into the combo boxes. self._viewer_combo_helpers = [] # This tracks the current positions of cube viewer axes when they are hidden self._viewer_axes_positions = [] # Indicates whether cube viewer toolbars are currently visible or not self._toolbars_visible = True self._slice_controller = SliceController(self) self._overlay_controller = OverlayController(self) self._units_controller = UnitController(self) # Add menu buttons to the cubeviz toolbar. self._init_menu_buttons() self.sync = {} # Track the slice index of the synced viewers. This is updated by the # slice controller self.synced_index = None app = get_qapp() app.installEventFilter(self) self._last_click = None self._active_view = None self._active_cube = None self._last_active_view = None self._active_split_cube = None # Set the default to parallel image viewer self._single_viewer_mode = False self.ui.button_toggle_image_mode.setText('Single Image Viewer') self.ui.viewer_control_frame.setCurrentIndex(0) # Add this class to the specviz dispatcher watcher dispatch.setup(self) def _init_menu_buttons(self): """ Add the two menu buttons to the tool bar. Currently two are defined: View - for changing the view of the active window Data Processing - for applying a data processing step to the data. :return: """ self._option_buttons = [ self.ui.view_option_button, self.ui.cube_option_button ] # Create the View Menu view_menu = self._dict_to_menu(OrderedDict([ ('Hide Axes', ['checkable', self._toggle_viewer_axes]), ('Hide Toolbars', ['checkable', self._toggle_toolbars]), ('Wavelength Units', lambda: self._open_dialog('Wavelength Units', None)) ])) self.ui.view_option_button.setMenu(view_menu) # Create the Data Processing Menu cube_menu = self._dict_to_menu(OrderedDict([ ('Collapse Cube', lambda: self._open_dialog('Collapse Cube', None)), ('Spatial Smoothing', lambda: self._open_dialog('Spatial Smoothing', None)), ('Moment Maps', lambda: self._open_dialog('Moment Maps', None)), ('Arithmetic Operations', lambda: self._open_dialog('Arithmetic Operations', None)) ])) self.ui.cube_option_button.setMenu(cube_menu) def _dict_to_menu(self, menu_dict): '''Stolen shamelessly from specviz. Thanks!''' menu_widget = QMenu() for k, v in menu_dict.items(): if isinstance(v, dict): new_menu = menu_widget.addMenu(k) self._dict_to_menu(v, menu_widget=new_menu) else: act = QAction(k, menu_widget) if isinstance(v, list): if v[0] == 'checkable': v = v[1] act.setCheckable(True) act.setChecked(False) act.triggered.connect(v) menu_widget.addAction(act) return menu_widget def _handle_settings_change(self, message): if isinstance(message, SettingsChangeMessage): self._slice_controller.update_index(self.synced_index) def _set_pos_and_margin(self, axes, pos, marg): axes.set_position(pos) freeze_margins(axes, marg) def _hide_viewer_axes(self): for viewer in self.cube_views: viewer._widget.toggle_hidden_axes(True) axes = viewer._widget.axes # Save current axes position and margins so they can be restored pos = axes.get_position(), axes.resizer.margins self._viewer_axes_positions.append(pos) self._set_pos_and_margin(axes, [0, 0, 1, 1], [0, 0, 0, 0]) viewer._widget.figure.canvas.draw() def _toggle_viewer_axes(self): # If axes are currently hidden, restore the original positions if self._viewer_axes_positions: for viewer, pos in zip(self.cube_views, self._viewer_axes_positions): viewer._widget.toggle_hidden_axes(False) axes = viewer._widget.axes self._set_pos_and_margin(axes, *pos) viewer._widget.figure.canvas.draw() self._viewer_axes_positions = [] # Record current positions if axes are currently hidden and hide them else: self._hide_viewer_axes() def _toggle_toolbars(self): self._toolbars_visible = not self._toolbars_visible for viewer in self.cube_views: viewer._widget.toolbar.setVisible(self._toolbars_visible) def _open_dialog(self, name, widget): if name == 'Collapse Cube': ex = collapse_cube.CollapseCube(self._data, parent=self, allow_preview=True) if name == 'Spatial Smoothing': ex = smoothing.SelectSmoothing(self._data, parent=self, allow_preview=True) if name == 'Arithmetic Operations': ex = arithmetic_gui.SelectArithmetic(self._data, self.session.data_collection, parent=self) if name == "Moment Maps": mm_gui = moment_maps.MomentMapsGUI( self._data, self.session.data_collection, parent=self) mm_gui.display() if name == 'Wavelength Units': current_unit = self._units_controller.units_titles.index(self._units_controller._new_units.long_names[0].title()) wavelength, ok_pressed = QInputDialog.getItem(self, "Pick a wavelength", "Wavelengths:", self._units_controller.units_titles, current_unit, False) if ok_pressed: self._units_controller.on_combobox_change(wavelength) @property def data_components(self): return self._data.main_components + self._data.derived_components @property def component_labels(self): return [str(cid) for cid in self.data_components] def refresh_viewer_combo_helpers(self): for i, helper in enumerate(self._viewer_combo_helpers): helper.refresh() @dispatch.register_listener("apply_operations") def apply_to_cube(self, stack): """ Listen for messages from specviz about possible spectral analysis operations that may be applied to the entire cube. """ # Retrieve the current cube data object operation_handler = SpectralOperationHandler(self._data, stack=stack, parent=self) operation_handler.exec_() def add_new_data_component(self, component_id): self.refresh_viewer_combo_helpers() if self._active_view in self.all_views: view_index = self.all_views.index(self._active_view) self.change_viewer_component(view_index, component_id) def remove_data_component(self, component_id): pass def _enable_option_buttons(self): for button in self._option_buttons: button.setEnabled(True) self.ui.sync_button.setEnabled(True) def _get_change_viewer_combo_func(self, combo, view_index): def _on_viewer_combo_change(dropdown_index): # This function gets called whenever one of the viewer combos gets # changed. The active combo is the one that comes from the parent # _get_change_viewer_combo_func function. # Find the relevant viewer viewer = self.all_views[view_index].widget() # Get the label of the component and the component ID itself label = combo.currentText() component = combo.currentData() viewer.has_2d_data = component.parent[label].ndim == 2 # If the user changed the current component, stop previewing # smoothing. if viewer.is_smoothing_preview_active: viewer.end_smoothing_preview() # Change the title and unit shown in the viwer viewer.update_component_unit_label(component) viewer.update_axes_title(title=str(label)) # Change the viewer's reference data to be the data containing the # current component. viewer.state.reference_data = component.parent # The viewer may have multiple layers, for instance layers for # the main cube and for any overlay datasets, as well as subset # layers. We go through all the layers and make sure that for the # layer which corresponds to the current dataset, the correct # attribute is shown. for layer_artist in viewer.layers: layer_state = layer_artist.state if layer_state.layer is component.parent: # We call _update_attribute here manually so that if this # function gets called before _update_attribute, it gets # called before we try and set the attribute below # (_update_attribute basically updates the internal list # of available attributes for the attribute combo) layer_state._update_attribute() layer_state.attribute = component # We then also make sure that this layer artist is the # one that is selected so that if the user uses e.g. the # contrast tool, it will change the right layer viewer._view.layer_list.select_artist(layer_artist) # If the combo corresponds to the currently active cube viewer, # either activate or deactivate the slice slider as appropriate. if self.all_views[view_index] is self._active_cube: self._slice_controller.set_enabled(not viewer.has_2d_data) # If contours are being currently shown, we need to force a redraw if viewer.is_contour_active: viewer.draw_contour() return _on_viewer_combo_change def _enable_viewer_combo(self, data, index, combo_label, selection_label): combo = getattr(self.ui, combo_label) connect_combo_selection(self, selection_label, combo) helper = ComponentIDComboHelper(self, selection_label) helper.set_multiple_data([data]) combo.setEnabled(True) combo.currentIndexChanged.connect(self._get_change_viewer_combo_func(combo, index)) self._viewer_combo_helpers.append(helper) def _enable_all_viewer_combos(self, data): """ Setup the dropdown boxes that correspond to each of the left, middle, and right views. The combo boxes initially are set to have FLUX, Error, DQ but will be dynamic depending on the type of data available either from being loaded in or by being processed. :return: """ self._enable_viewer_combo( data, 0, 'single_viewer_combo', 'single_viewer_attribute') view = self.all_views[0].widget() component = getattr(self, 'single_viewer_attribute') view.update_component_unit_label(component) view.update_axes_title(component.label) for i in range(1,4): combo_label = 'viewer{0}_combo'.format(i) selection_label = 'viewer{0}_attribute'.format(i) self._enable_viewer_combo(data, i, combo_label, selection_label) view = self.all_views[i].widget() component = getattr(self, selection_label) view.update_component_unit_label(component) view.update_axes_title(component.label) def change_viewer_component(self, view_index, component_id, force=False): """ Given a viewer at an index view_index, change combo selection to component at an index component_index. :param view_index: int: Viewer index :param component_id: ComponentID: Component ID in viewer combo :param force: bool: force change if component is already displayed. """ combo = self.get_viewer_combo(view_index) component_index = combo.findData(component_id) if combo.currentIndex() == component_index and force: combo.currentIndexChanged.emit(component_index) else: combo.setCurrentIndex(component_index) def get_viewer_combo(self, view_index): """ Get viewer combo for a given viewer index """ if view_index == 0: combo_label = 'single_viewer_combo' else: combo_label = 'viewer{0}_combo'.format(view_index) return getattr(self.ui, combo_label) def add_overlay(self, data, label): self._overlay_controller.add_overlay(data, label) def add_data(self, data): """ Called by a function outside the class in order to add data to cubeviz. :param data: :return: """ self._data = data self.specviz._widget.add_data(data) cid = self.specviz._widget._options_widget.file_att dispatch.changed_units.emit(y=data.get_component(cid).units) for checkbox in self._synced_checkboxes: checkbox.setEnabled(True) self._has_data = True self._active_view = self.left_view self._active_cube = self.left_view self._last_active_view = self.single_view self._active_split_cube = self.left_view # Store pointer to wavelength information self._wavelengths = self.single_view._widget._data[0].coords.world_axis(self.single_view._widget._data[0], axis=0) # Pass WCS and wavelength information to slider controller and enable wcs = self.session.data_collection.data[0].coords.wcs self._slice_controller.enable(wcs, self._wavelengths) self._units_controller.enable(wcs, self._wavelengths) self._enable_option_buttons() self._setup_syncing() self._enable_all_viewer_combos(data) self.subWindowActivated.emit(self._active_view) def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.MouseButtonPress: if not (self.isVisible() and self.isActiveWindow()): return super(CubeVizLayout, self).eventFilter(obj, event) # Find global click position click_pos = event.globalPos() # If the click position is the same as the last one, we shouldn't # do anything. if click_pos != self._last_click: # Determine if the event falls inside any of the viewers for viewer in self.subWindowList(): relative_click_pos = viewer.mapFromGlobal(click_pos) if viewer.rect().contains(relative_click_pos): # We should only emit an event if the active subwindow # has actually changed. if viewer is not self.activeSubWindow(): self.subWindowActivated.emit(viewer) break self._last_click = click_pos return super(CubeVizLayout, self).eventFilter(obj, event) def _toggle_image_mode(self, event=None): new_active_view = self._last_active_view self._last_active_view = self._active_view # Currently in single image, moving to split image if self._single_viewer_mode: self._active_cube = self._active_split_cube self._activate_split_image_mode(event) self._single_viewer_mode = False self.ui.button_toggle_image_mode.setText('Single Image Viewer') self.ui.viewer_control_frame.setCurrentIndex(0) for view in self.split_views: if self.single_view._widget.synced: if view._widget.synced: view._widget.update_slice_index(self.single_view._widget.slice_index) view._widget.update() # Currently in split image, moving to single image else: self._active_split_cube = self._active_cube self._active_view = self.single_view self._active_cube = self.single_view self._activate_single_image_mode(event) self._single_viewer_mode = True self.ui.button_toggle_image_mode.setText('Split Image Viewer') self.ui.viewer_control_frame.setCurrentIndex(1) self._active_view._widget.update() self.subWindowActivated.emit(new_active_view) # Update the slice index to reflect the state of the active cube self._slice_controller.update_index(self._active_cube._widget.slice_index) def _activate_single_image_mode(self, event=None): vsplitter = self.ui.vertical_splitter hsplitter = self.ui.horizontal_splitter vsizes = list(vsplitter.sizes()) hsizes = list(hsplitter.sizes()) vsizes = 0, max(10, vsizes[0] + vsizes[1]) hsizes = max(10, sum(hsizes) * 0.4), max(10, sum(hsizes) * 0.6) vsplitter.setSizes(vsizes) hsplitter.setSizes(hsizes) def _activate_split_image_mode(self, event=None): vsplitter = self.ui.vertical_splitter hsplitter = self.ui.horizontal_splitter vsizes = list(vsplitter.sizes()) hsizes = list(hsplitter.sizes()) vsizes = max(10, sum(vsizes) / 2), max(10, sum(vsizes) / 2) # TODO: Might be a bug here, should the hsizes be based on vsizes? If so, not sure we need to calculate # TODO: the hsizes above. hsizes = 0, max(10, vsizes[0] + vsizes[1]) vsplitter.setSizes(vsizes) hsplitter.setSizes(hsizes) def _update_active_view(self, view): if self._has_data: self._active_view = view if isinstance(view._widget, CubevizImageViewer): self._active_cube = view index = self._active_cube._widget.slice_index if view._widget.has_2d_data: self._slice_controller.set_enabled(False) else: self._slice_controller.set_enabled(True) self._slice_controller.update_index(index) def activeSubWindow(self): return self._active_view def subWindowList(self): return [self.single_view, self.left_view, self.middle_view, self.right_view, self.specviz] def _setup_syncing(self): for attribute in ['x_min', 'x_max', 'y_min', 'y_max']: sync1 = keep_in_sync(self.left_view._widget.state, attribute, self.middle_view._widget.state, attribute) sync2 = keep_in_sync(self.middle_view._widget.state, attribute, self.right_view._widget.state, attribute) self.sync[attribute] = sync1, sync2 self._on_sync_click() def _on_sync_click(self, event=None): index = self._active_cube._widget.slice_index for view in self.cube_views: view._widget.synced = True if view != self._active_cube: view._widget.update_slice_index(index) self._slice_controller.update_index(index) def start_smoothing_preview(self, preview_function, component_id, preview_title=None): """ Starts smoothing preview. This function preforms the following steps 1) SelectSmoothing passes parameters. 2) The left and single viewers' combo box is set to component_id 3) The set_smoothing_preview is called to setup on the fly smoothing :param preview_function: function: Single-slice smoothing function :param component_id: int: Which component to preview :param preview_title: str: Title displayed when previewing """ # For single and first viewer: self._original_components = {} for view_index in [0, 1]: combo = self.get_viewer_combo(view_index) self._original_components[view_index] = combo.currentData() view = self.all_views[view_index].widget() self.change_viewer_component(view_index, component_id, force=True) view.set_smoothing_preview(preview_function, preview_title) def end_smoothing_preview(self): """ End preview and change viewer combo index to the first component. """ for view_index in [0, 1]: view = self.all_views[view_index].widget() view.end_smoothing_preview() if view_index in self._original_components: component_id = self._original_components[view_index] self.change_viewer_component(view_index, component_id, force=True) self._original_components = {} def showEvent(self, event): super(CubeVizLayout, self).showEvent(event) # Make split image mode the default layout self._activate_split_image_mode() self._update_active_view(self.left_view) def change_slice_index(self, amount): self._slice_controller.change_slider_value(amount) def get_wavelengths(self): return self._wavelengths def get_wavelengths_units(self): return self._units_controller.get_new_units() def set_wavelengths(self, new_wavelengths, new_units): self._wavelengths = new_wavelengths self._slice_controller.set_wavelengths(new_wavelengths, new_units)
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 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)
class ComponentManagerWidget(QtWidgets.QDialog): data = SelectionCallbackProperty() def __init__(self, data_collection=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) self.ui.combosel_data.setCurrentIndex(0) 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 CubeVizLayout(QtWidgets.QWidget): """ The 'CubeViz' layout, with three image viewers and one spectrum viewer. """ LABEL = "CubeViz" subWindowActivated = QtCore.Signal(object) single_viewer_attribute = SelectionCallbackProperty(default_index=0) viewer1_attribute = SelectionCallbackProperty(default_index=0) viewer2_attribute = SelectionCallbackProperty(default_index=1) viewer3_attribute = SelectionCallbackProperty(default_index=2) def __init__(self, session=None, parent=None): super(CubeVizLayout, self).__init__(parent=parent) self._cubeviz_toolbar = None if not getattr(session.application, '_has_cubeviz_toolbar', False): self._cubeviz_toolbar = CubevizToolbar( application=session.application) session.application.insertToolBar( session.application._data_toolbar, self._cubeviz_toolbar) session.application._has_cubeviz_toolbar = True self.session = session self._has_data = False self._option_buttons = [] self._data = None self.ui = load_ui('layout.ui', self, directory=os.path.dirname(__file__)) self.cube_views = [] # Create the cube viewers and register to the hub. for _ in range(DEFAULT_NUM_SPLIT_VIEWERS + 1): ww = WidgetWrapper(CubevizImageViewer(self.session, cubeviz_layout=self), tab_widget=self, toolbar=True) self.cube_views.append(ww) ww._widget.register_to_hub(self.session.hub) self.set_toolbar_icon_size(DEFAULT_TOOLBAR_ICON_SIZE) # Create specviz viewer and register to the hub. self.specviz = WidgetWrapper(SpecvizDataViewer(self.session, layout=self, include_line=True), tab_widget=self, toolbar=False) self.specviz._widget.register_to_hub(self.session.hub) self.single_view = self.cube_views[0] self.split_views = self.cube_views[1:] self._synced_checkboxes = [view.checkbox for view in self.cube_views] for view, checkbox in zip(self.cube_views, self._synced_checkboxes): view._widget.assign_synced_checkbox(checkbox) # Add the views to the layouts. self.ui.single_image_layout.addWidget(self.single_view) for viewer in self.split_views: self.ui.image_row_layout.addWidget(viewer) self.ui.specviz_layout.addWidget(self.specviz) self.subWindowActivated.connect(self._update_active_view) self.ui.sync_button.clicked.connect(self._on_sync_click) self.ui.button_toggle_image_mode.clicked.connect( self._toggle_image_mode) # This is a list of helpers for the viewer combo boxes. New data # collections should be added to each helper in this list using the # ``append_data`` method to ensure that the new data components are # populated into the combo boxes. self._viewer_combo_helpers = [] # This tracks the current positions of cube viewer axes when they are hidden self._viewer_axes_positions = [] # Indicates whether cube viewer toolbars are currently visible or not self._toolbars_visible = True # Indicates whether subset stats should be displayed or not self._stats_visible = True self._slice_controller = SliceController(self) self._overlay_controller = OverlayController(self) self._wavelength_controller = WavelengthController(self) self._flux_unit_controller = FluxUnitController(self) # Add menu buttons to the cubeviz toolbar. self.ra_dec_format_menu = None self._init_menu_buttons() self.sync = {} app = get_qapp() app.installEventFilter(self) self._last_click = None self._active_view = None self._active_cube = None self._last_active_view = None self._active_split_cube = None # Set the default to parallel image viewer self._single_viewer_mode = False self.ui.button_toggle_image_mode.setText('Single Image Viewer') # Listen for unit changes in specviz self.specviz._widget.hub.plot_widget.spectral_axis_unit_changed.connect( self._wavelength_controller.update_units) def _update_displayed_units(unit): """ Re-create minimum flux unit change functionality without having to spawn the ``QDialog`` object in cubeviz. """ component_id = self.specviz._widget.layers[0].state.attribute cubeviz_unit = self._flux_unit_controller.add_component_unit( component_id, unit) self._flux_unit_controller.data.get_component( component_id).units = unit msg = FluxUnitsUpdateMessage(self, cubeviz_unit, component_id) self._wavelength_controller._hub.broadcast(msg) self.specviz._widget.hub.plot_widget.data_unit_changed.connect( _update_displayed_units) def _init_menu_buttons(self): """ Add the two menu buttons to the tool bar. Currently two are defined: View - for changing the view of the active window Data Processing - for applying a data processing step to the data. :return: """ self._option_buttons = [ self.ui.view_option_button, self.ui.cube_option_button ] # Create the View Menu view_menu = self._dict_to_menu( OrderedDict([ ('Hide Axes', ['checkable', self._toggle_viewer_axes]), ('Hide Toolbars', ['checkable', self._toggle_toolbars]), ('Hide Spaxel Value Tooltip', ['checkable', self._toggle_hover_value]), ('Hide Stats', ['checkable', self._toggle_stats_display]), ('Flux Units', OrderedDict([ ('Convert Displayed Units', lambda: self._open_dialog( 'Convert Displayed Units', None)), ('Convert Data Values', lambda: self._open_dialog('Convert Data Values', None)), ])), ('Wavelength Units/Redshift', lambda: self._open_dialog('Wavelength Units/Redshift', None)) ])) # Add toggle RA-DEC format: format_menu = view_menu.addMenu("RA-DEC Format") format_action_group = QActionGroup(format_menu) self.ra_dec_format_menu = format_menu # Make sure to change all instances of the the names # of the formats if modifications are made to them. for format_name in ["Sexagesimal", "Decimal Degrees"]: act = QAction(format_name, format_menu) act.triggered.connect(self._toggle_all_coords_in_degrees) act.setActionGroup(format_action_group) act.setCheckable(True) act.setChecked( True) if format == "Sexagesimal" else act.setChecked(False) format_menu.addAction(act) self.ui.view_option_button.setMenu(view_menu) # Create the Data Processing Menu cube_menu = self._dict_to_menu( OrderedDict([ ('Collapse Cube', lambda: self._open_dialog('Collapse Cube', None)), ('Spatial Smoothing', lambda: self._open_dialog('Spatial Smoothing', None)), ('Moment Maps', lambda: self._open_dialog('Moment Maps', None)), ('Arithmetic Operations', lambda: self._open_dialog('Arithmetic Operations', None)) ])) self.ui.cube_option_button.setMenu(cube_menu) def _dict_to_menu(self, menu_dict, menu_widget=None): '''Stolen shamelessly from specviz. Thanks!''' if not menu_widget: menu_widget = QMenu() for k, v in menu_dict.items(): if isinstance(v, dict): new_menu = menu_widget.addMenu(k) self._dict_to_menu(v, menu_widget=new_menu) else: act = QAction(k, menu_widget) if isinstance(v, list): if v[0] == 'checkable': v = v[1] act.setCheckable(True) act.setChecked(False) act.triggered.connect(v) menu_widget.addAction(act) return menu_widget def set_toolbar_icon_size(self, size): for view in self.cube_views: view._widget.toolbar.setIconSize(QtCore.QSize(size, size)) def handle_settings_change(self, message): if isinstance(message, SettingsChangeMessage): self._slice_controller.update_index(self.synced_index) @property def synced_index(self): return self._slice_controller.synced_index def handle_subset_action(self, message): self.refresh_viewer_combo_helpers() if isinstance(message, SubsetUpdateMessage): for combo, viewer in zip(self._viewer_combo_helpers, self.cube_views): viewer._widget.show_roi_stats(combo.selection, message.subset) elif isinstance(message, SubsetDeleteMessage): for viewer in self.cube_views: viewer._widget.show_slice_stats() elif isinstance(message, EditSubsetMessage) and not message.subset: for viewer in self.cube_views: viewer._widget.show_slice_stats() def _set_pos_and_margin(self, axes, pos, marg): axes.set_position(pos) freeze_margins(axes, marg) def _hide_viewer_axes(self): for viewer in self.cube_views: viewer._widget.toggle_hidden_axes(True) axes = viewer._widget.axes # Save current axes position and margins so they can be restored pos = axes.get_position(), axes.resizer.margins self._viewer_axes_positions.append(pos) self._set_pos_and_margin(axes, [0, 0, 1, 1], [0, 0, 0, 0]) viewer._widget.figure.canvas.draw() def _toggle_viewer_axes(self): # If axes are currently hidden, restore the original positions if self._viewer_axes_positions: for viewer, pos in zip(self.cube_views, self._viewer_axes_positions): viewer._widget.toggle_hidden_axes(False) axes = viewer._widget.axes self._set_pos_and_margin(axes, *pos) viewer._widget.figure.canvas.draw() self._viewer_axes_positions = [] # Record current positions if axes are currently hidden and hide them else: self._hide_viewer_axes() def _toggle_toolbars(self): self._toolbars_visible = not self._toolbars_visible for viewer in self.cube_views: viewer._widget.toolbar.setVisible(self._toolbars_visible) def _toggle_hover_value(self): for viewer in self.cube_views: viewer._widget._is_tooltip_on = not viewer._widget._is_tooltip_on def _toggle_stats_display(self): self._stats_visible = not self._stats_visible for viewer in self.cube_views: viewer.set_stats_visible(self._stats_visible) def _open_dialog(self, name, widget): if name == 'Collapse Cube': ex = collapse_cube.CollapseCube( self._wavelength_controller.wavelengths, self._wavelength_controller.current_units, self._data, parent=self, allow_preview=True) if name == 'Spatial Smoothing': ex = smoothing.SelectSmoothing(self._data, parent=self, allow_preview=True) if name == 'Arithmetic Operations': dialog = ArithmeticEditorWidget(self.session.data_collection) dialog.exec_() if name == "Moment Maps": mm_gui = moment_maps.MomentMapsGUI(self._data, self.session.data_collection, parent=self) mm_gui.display() if name == 'Convert Displayed Units': self._flux_unit_controller.converter(parent=self) if name == 'Convert Data Values': self._flux_unit_controller.converter(parent=self, convert_data=True) if name == "Wavelength Units/Redshift": WavelengthUI(self._wavelength_controller, parent=self) def refresh_flux_units(self, message): """ Listens for flux unit update messages (this is called from `listeners`) and updates the displayed spectral units in the specviz data viewer. """ unit = message.flux_units self.specviz._widget.hub.plot_widget.data_unit = unit.to_string() def _toggle_all_coords_in_degrees(self): """ Switch ra-dec b/w "Sexagesimal" and "Decimal Degrees" """ menu = self.ra_dec_format_menu for action in menu.actions(): if "Decimal Degrees" == action.text(): coords_in_degrees = action.isChecked() break for view in self.cube_views: viewer = view.widget() if viewer._coords_in_degrees != coords_in_degrees: viewer.toggle_coords_in_degrees() @property def data_components(self): return self._data.main_components + self._data.derived_components @property def component_labels(self): return [str(cid) for cid in self.data_components] def refresh_viewer_combo_helpers(self): for i, helper in enumerate(self._viewer_combo_helpers): helper.refresh() def remove_data_component(self, component_id): pass def _enable_option_buttons(self): for button in self._option_buttons: button.setEnabled(True) self.ui.sync_button.setEnabled(True) def _get_change_viewer_combo_func(self, combo, view_index): def _on_viewer_combo_change(dropdown_index): # This function gets called whenever one of the viewer combos gets # changed. The active combo is the one that comes from the parent # _get_change_viewer_combo_func function. # Find the relevant viewer viewer = self.cube_views[view_index].widget() # Get the label of the component and the component ID itself label = combo.currentText() component = combo.currentData() if isinstance(component, UserDataWrapper): component = component.data viewer.has_2d_data = component.parent[label].ndim == 2 # If the user changed the current component, stop previewing # smoothing. if viewer.is_smoothing_preview_active: viewer.end_smoothing_preview() # Change the component label, title and unit shown in the viewer viewer.current_component_id = component viewer.cubeviz_unit = self._flux_unit_controller[component] viewer.update_axes_title(title=str(label)) # Change the viewer's reference data to be the data containing the # current component. viewer.state.reference_data = component.parent # The viewer may have multiple layers, for instance layers for # the main cube and for any overlay datasets, as well as subset # layers. We go through all the layers and make sure that for the # layer which corresponds to the current dataset, the correct # attribute is shown. for layer_artist in viewer.layers: layer_state = layer_artist.state if layer_state.layer is component.parent: # We call _update_attribute here manually so that if this # function gets called before _update_attribute, it gets # called before we try and set the attribute below # (_update_attribute basically updates the internal list # of available attributes for the attribute combo) layer_state._update_attribute() layer_state.attribute = component # We then also make sure that this layer artist is the # one that is selected so that if the user uses e.g. the # contrast tool, it will change the right layer viewer._view.layer_list.select_artist(layer_artist) # If the combo corresponds to the currently active cube viewer, # either activate or deactivate the slice slider as appropriate. if self.cube_views[view_index] is self._active_cube: self._slice_controller.set_enabled(not viewer.has_2d_data) # If contours are being currently shown, we need to force a redraw if viewer.is_contour_active: viewer.draw_contour() viewer.update_component(component) return _on_viewer_combo_change def _enable_viewer_combo(self, viewer, data, index, selection_label): connect_combo_selection(self, selection_label, viewer.combo) helper = ComponentIDComboHelper(self, selection_label) helper.set_multiple_data([data]) viewer.combo.setEnabled(True) viewer.combo.currentIndexChanged.connect( self._get_change_viewer_combo_func(viewer.combo, index)) self._viewer_combo_helpers.append(helper) def _enable_all_viewer_combos(self, data): """ Setup the dropdown boxes that correspond to each of the left, middle, and right views. The combo boxes initially are set to have FLUX, Error, DQ but will be dynamic depending on the type of data available either from being loaded in or by being processed. :return: """ view = self.cube_views[0].widget() self._enable_viewer_combo(view.parent(), data, 0, 'single_viewer_attribute') component = self.single_viewer_attribute view.current_component_id = component view.cubeviz_unit = self._flux_unit_controller[component] view.update_axes_title(component.label) for i in range(1, 4): view = self.cube_views[i].widget() selection_label = 'viewer{0}_attribute'.format(i) self._enable_viewer_combo(view.parent(), data, i, selection_label) component = getattr(self, selection_label) view.current_component_id = component view.cubeviz_unit = self._flux_unit_controller[component] view.update_axes_title(component.label) def change_viewer_component(self, view_index, component_id, force=False): """ Given a viewer at an index view_index, change combo selection to component at an index component_index. :param view_index: int: Viewer index :param component_id: ComponentID: Component ID in viewer combo :param force: bool: force change if component is already displayed. """ combo = self.get_viewer_combo(view_index) try: if isinstance(component_id, str): component_index = _find_combo_text(combo, component_id) else: component_index = _find_combo_data(combo, component_id) except ValueError: component_index = -1 if combo.currentIndex() == component_index and force: combo.currentIndexChanged.emit(component_index) else: combo.setCurrentIndex(component_index) def display_component(self, component_id): """ Displays data with given component ID in the active cube viewer. """ self.refresh_viewer_combo_helpers() if self._single_viewer_mode: self.change_viewer_component(0, component_id) else: self.change_viewer_component(1, component_id) def get_viewer_combo(self, view_index): """ Get viewer combo for a given viewer index """ return self.cube_views[view_index].combo def add_overlay(self, data, label, display_now=True): self._overlay_controller.add_overlay(data, label, display=display_now) self.display_component(label) def _set_data_coord_system(self, data): """ Check if data coordinates are in RA-DEC first. Then set viewers to the default coordinate system. :param data: input data """ is_ra_dec = isinstance(wcs_to_celestial_frame(data.coords.wcs), BaseRADecFrame) self.ra_dec_format_menu.setDisabled(not is_ra_dec) if not is_ra_dec: return is_coords_in_degrees = False for view in self.cube_views: viewer = view.widget() viewer.init_ra_dec() is_coords_in_degrees = viewer._coords_in_degrees if is_coords_in_degrees: format_name = "Decimal Degrees" else: format_name = "Sexagesimal" menu = self.ra_dec_format_menu for action in menu.actions(): if format_name == action.text(): action.setChecked(True) break def add_data(self, data): """ Called by a function outside the class in order to add data to cubeviz. :param data: :return: """ self._data = data self.specviz._widget.add_data(data) self._flux_unit_controller.set_data(data) for checkbox in self._synced_checkboxes: checkbox.setEnabled(True) self._has_data = True self._active_view = self.split_views[0] self._active_cube = self.split_views[0] self._last_active_view = self.single_view self._active_split_cube = self.split_views[0] # Store pointer to wcs and wavelength information wcs = self.session.data_collection.data[0].coords.wcs wavelengths = self.single_view._widget._data[0].coords.world_axis( self.single_view._widget._data[0], axis=0) self._enable_all_viewer_combos(data) # TODO: currently this way of accessing units is not flexible self._slice_controller.enable() self._wavelength_controller.enable(str(wcs.wcs.cunit[2]), wavelengths) self._enable_option_buttons() self._setup_syncing() for viewer in self.cube_views: viewer.slice_text.setText('slice: {:5}'.format(self.synced_index)) self.subWindowActivated.emit(self._active_view) # Check if coord system is RA and DEC (ie not galactic etc..) self._set_data_coord_system(data) def eventFilter(self, obj, event): if isinstance(event, QtGui.QMouseEvent): if not (self.isVisible() and self.isActiveWindow()): return super(CubeVizLayout, self).eventFilter(obj, event) # Find global click position click_pos = event.globalPos() # If the click position is the same as the last one, we shouldn't # do anything. if click_pos != self._last_click: # Determine if the event falls inside any of the viewers for viewer in self.subWindowList(): relative_click_pos = viewer.mapFromGlobal(click_pos) if viewer.rect().contains(relative_click_pos): # We should only emit an event if the active subwindow # has actually changed. if viewer is not self.activeSubWindow(): self.subWindowActivated.emit(viewer) break self._last_click = click_pos return super(CubeVizLayout, self).eventFilter(obj, event) def _toggle_image_mode(self, event=None): new_active_view = self._last_active_view self._last_active_view = self._active_view # Currently in single image, moving to split image if self._single_viewer_mode: self._active_cube = self._active_split_cube self._activate_split_image_mode(event) self._single_viewer_mode = False self.ui.button_toggle_image_mode.setText('Single Image Viewer') for view in self.split_views: if self.single_view._widget.synced: if view._widget.synced: view._widget.update_slice_index( self.single_view._widget.slice_index) view._widget.update() # Currently in split image, moving to single image else: self._active_split_cube = self._active_cube self._active_view = self.single_view self._active_cube = self.single_view self._activate_single_image_mode(event) self._single_viewer_mode = True self.ui.button_toggle_image_mode.setText('Split Image Viewer') self._active_view._widget.update() self.subWindowActivated.emit(new_active_view) # Update the slice index to reflect the state of the active cube self._slice_controller.update_index( self._active_cube._widget.slice_index) def _activate_single_image_mode(self, event=None): vsplitter = self.ui.vertical_splitter hsplitter = self.ui.horizontal_splitter vsizes = list(vsplitter.sizes()) hsizes = list(hsplitter.sizes()) vsizes = 0, max(10, vsizes[0] + vsizes[1]) hsizes = max(10, sum(hsizes) * 0.4), max(10, sum(hsizes) * 0.6) vsplitter.setSizes(vsizes) hsplitter.setSizes(hsizes) def _activate_split_image_mode(self, event=None): vsplitter = self.ui.vertical_splitter hsplitter = self.ui.horizontal_splitter vsizes = list(vsplitter.sizes()) hsizes = list(hsplitter.sizes()) vsizes = max(10, sum(vsizes) / 2), max(10, sum(vsizes) / 2) # TODO: Might be a bug here, should the hsizes be based on vsizes? If so, not sure we need to calculate # TODO: the hsizes above. hsizes = 0, max(10, vsizes[0] + vsizes[1]) vsplitter.setSizes(vsizes) hsplitter.setSizes(hsizes) def _update_active_view(self, view): if self._has_data: self._active_view = view if isinstance(view._widget, CubevizImageViewer): self._active_cube = view index = self._active_cube._widget.slice_index if view._widget.has_2d_data: self._slice_controller.set_enabled(False) else: self._slice_controller.set_enabled(True) self._slice_controller.update_index(index) def activeSubWindow(self): return self._active_view def subWindowList(self): return self.cube_views + [self.specviz] def _setup_syncing(self): for attribute in ['x_min', 'x_max', 'y_min', 'y_max']: # TODO: this will need to be generalized if we want to support an # arbitrary number of viewers. sync1 = keep_in_sync(self.split_views[0]._widget.state, attribute, self.split_views[1]._widget.state, attribute) sync2 = keep_in_sync(self.split_views[1]._widget.state, attribute, self.split_views[2]._widget.state, attribute) self.sync[attribute] = sync1, sync2 self._on_sync_click() def _on_sync_click(self, event=None): index = self._active_cube._widget.slice_index for view in self.cube_views: view._widget.synced = True if view != self._active_cube: view._widget.update_slice_index(index) self._slice_controller.update_index(index) def start_smoothing_preview(self, preview_function, component_id, preview_title=None): """ Starts smoothing preview. This function preforms the following steps 1) SelectSmoothing passes parameters. 2) The left and single viewers' combo box is set to component_id 3) The set_smoothing_preview is called to setup on the fly smoothing :param preview_function: function: Single-slice smoothing function :param component_id: int: Which component to preview :param preview_title: str: Title displayed when previewing """ # For single and first viewer: self._original_components = {} for view_index in [0, 1]: combo = self.get_viewer_combo(view_index) self._original_components[view_index] = combo.currentData() view = self.cube_views[view_index].widget() self.change_viewer_component(view_index, component_id, force=True) view.set_smoothing_preview(preview_function, preview_title) def end_smoothing_preview(self): """ End preview and change viewer combo index to the first component. """ for view_index in [0, 1]: view = self.cube_views[view_index].widget() view.end_smoothing_preview() if view_index in self._original_components: component_id = self._original_components[view_index] self.change_viewer_component(view_index, component_id, force=True) self._original_components = {} def showEvent(self, event): super(CubeVizLayout, self).showEvent(event) # Make split image mode the default layout self._activate_split_image_mode() self._update_active_view(self.split_views[0]) def change_slice_index(self, amount): self._slice_controller.change_slider_value(amount) def get_wavelengths(self): return self._wavelength_controller.wavelengths def get_wavelengths_units(self): return self._wavelength_controller.current_units def get_wavelength(self, index=None): if index is None: index = self.synced_index elif index > len(self.get_wavelengths()): return None wave = self.get_wavelengths()[index] units = self.get_wavelengths_units() return wave * units
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 LinkEditorState(State): data1 = SelectionCallbackProperty() data2 = SelectionCallbackProperty() current_link = SelectionCallbackProperty() link_type = SelectionCallbackProperty() restrict_to_suggested = CallbackProperty(False) def __init__(self, data_collection, suggested_links=None): super(LinkEditorState, self).__init__() self.data1_helper = DataCollectionComboHelper(self, 'data1', data_collection) self.data2_helper = DataCollectionComboHelper(self, 'data2', data_collection) # 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.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.data_in is self.data1 and link.data_out is self.data2) or (link.data_in is self.data2 and link.data_out is self.data1)): links.append(link) return links 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() 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() 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 new_link(self, function_or_helper): if hasattr(function_or_helper, 'function'): link = EditableLinkFunctionState(function_or_helper.function, data_in=self.data1, data_out=self.data2, output_names=function_or_helper.output_labels, description=function_or_helper.info, display=function_or_helper.function.__name__) else: link = EditableLinkFunctionState(function_or_helper.helper, data_in=self.data1, data_out=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)