def _add_plot_type_option_menu(self, menu, ax): with errorbar_caps_removed(ax): # Able to change the plot type to waterfall if there is only one axes, it is a MantidAxes, and there is more # than one line on the axes. if len(ax.get_figure().get_axes()) > 1 or not isinstance( ax, MantidAxes) or len(ax.get_lines()) <= 1: return plot_type_menu = QMenu("Plot Type", menu) plot_type_action_group = QActionGroup(plot_type_menu) standard = plot_type_menu.addAction( "1D", lambda: self._change_plot_type( ax, plot_type_action_group.checkedAction())) waterfall = plot_type_menu.addAction( "Waterfall", lambda: self._change_plot_type( ax, plot_type_action_group.checkedAction())) for action in [waterfall, standard]: plot_type_action_group.addAction(action) action.setCheckable(True) if ax.is_waterfall(): waterfall.setChecked(True) else: standard.setChecked(True) menu.addMenu(plot_type_menu)
def _add_marker_option_menu(self, menu, event): """ Entry in main context menu to: - add horizontal/vertical markers - open a marker editor window. The editor window allows editing of all currently plotted markers :param menu: instance of QMenu to append this submenu to :param event: mpl event that generated the call """ marker_menu = QMenu("Markers", menu) marker_action_group = QActionGroup(marker_menu) x0, x1 = event.inaxes.get_xlim() y0, y1 = event.inaxes.get_ylim() horizontal = marker_menu.addAction( "Horizontal", lambda: self._add_horizontal_marker( event.ydata, y0, y1, event.inaxes)) vertical = marker_menu.addAction( "Vertical", lambda: self._add_vertical_marker( event.xdata, x0, x1, event.inaxes)) edit = marker_menu.addAction("Edit", lambda: self._global_edit_markers()) for action in [horizontal, vertical, edit]: marker_action_group.addAction(action) menu.addMenu(marker_menu)
def _add_plot_type_option_menu(self, menu, ax): # Error bar caps are considered lines so they are removed before checking the number of lines on the axes so # they aren't confused for "actual" lines. error_bar_caps = datafunctions.remove_and_return_errorbar_cap_lines(ax) # Able to change the plot type to waterfall if there is only one axes, it is a MantidAxes, and there is more # than one line on the axes. if len(ax.get_figure().get_axes()) > 1 or not isinstance( ax, MantidAxes) or len(ax.get_lines()) <= 1: return # Re-add error bar caps ax.lines += error_bar_caps plot_type_menu = QMenu("Plot Type", menu) plot_type_action_group = QActionGroup(plot_type_menu) standard = plot_type_menu.addAction( "1D", lambda: self._change_plot_type( ax, plot_type_action_group.checkedAction())) waterfall = plot_type_menu.addAction( "Waterfall", lambda: self._change_plot_type( ax, plot_type_action_group.checkedAction())) for action in [waterfall, standard]: plot_type_action_group.addAction(action) action.setCheckable(True) if ax.is_waterfall(): waterfall.setChecked(True) else: standard.setChecked(True) menu.addMenu(plot_type_menu)
def _add_normalization_option_menu(self, menu, ax): # Check if toggling normalization makes sense can_toggle_normalization = self._can_toggle_normalization(ax) if not can_toggle_normalization: return None # Create menu norm_menu = QMenu("Normalization", menu) norm_actions_group = QActionGroup(norm_menu) none_action = norm_menu.addAction( 'None', lambda: self._set_normalization_none(ax)) norm_action = norm_menu.addAction( 'Bin Width', lambda: self._set_normalization_bin_width(ax)) for action in [none_action, norm_action]: norm_actions_group.addAction(action) action.setCheckable(True) # Update menu state is_normalized = self._is_normalized(ax) if is_normalized: norm_action.setChecked(True) else: none_action.setChecked(True) menu.addMenu(norm_menu)
def _add_axes_scale_menu(self, menu): """Add the Axes scale options menu to the given menu""" axes_menu = QMenu("Axes", menu) axes_actions = QActionGroup(axes_menu) current_scale_types = self._get_axes_scale_types() for label, scale_types in iteritems(AXES_SCALE_MENU_OPTS): action = axes_menu.addAction(label, partial(self._quick_change_axes, scale_types)) if current_scale_types == scale_types: action.setCheckable(True) action.setChecked(True) axes_actions.addAction(action) menu.addMenu(axes_menu)
def _add_colorbar_axes_scale_menu(self, menu, ax): """Add the Axes scale options menu to the given menu""" axes_menu = QMenu("Color bar", menu) axes_actions = QActionGroup(axes_menu) images = ax.get_images() + [col for col in ax.collections if isinstance(col, Collection)] for label, scale_type in COLORBAR_SCALE_MENU_OPTS.items(): action = axes_menu.addAction(label, partial(self._change_colorbar_axes, scale_type)) if type(images[0].norm) == scale_type: action.setCheckable(True) action.setChecked(True) axes_actions.addAction(action) menu.addMenu(axes_menu)
def _single_marker_menu(self, menu, marker): """ Entry in a menu that allows editing/deleting a single marker :param menu: instance of QMenu to add this submenu to :param marker: marker to be edited with the menu """ marker_menu = QMenu(marker.name, menu) marker_action_group = QActionGroup(marker_menu) delete = marker_menu.addAction("Delete", lambda: self._delete_marker(marker)) edit = marker_menu.addAction("Edit", lambda: self._edit_marker(marker)) for action in [delete, edit]: marker_action_group.addAction(action) menu.addMenu(marker_menu)
def __init__(self, *args, menuBar, **kwargs): super().__init__(*args, **kwargs) self._run_to_tabs = collections.defaultdict(list) self._title_to_tab = {} self._tabs_from_streaming = [] self._overplot = OverPlotState.individual_tab self._overplot_target = None self._live_enabled = False self._live_run_router = RunRouter([self.route_live_stream]) self._containers = [TabbedViewingArea(self, menuBar=menuBar) for _ in range(2)] layout = QVBoxLayout() for container in self._containers: layout.addWidget(container) self.setLayout(layout) overplot_group = QActionGroup(self) self.off = QAction('&Off', self) self.off.setStatusTip('Drop streaming data.') self.individual_tab = QAction('&New Tab', self) self.individual_tab.setStatusTip('Open a new viewer tab for each Run.') self.latest_live = QAction('&Latest Live Tab', self) self.latest_live.setStatusTip('Attempt to overplot on the most recent live Run.') self.fixed = QAction('&Fixed Tab...', self) self.fixed.setStatusTip('Attempt to overplot on a specific tab.') self.fixed.setEnabled(False) overplot_group.addAction(self.off) overplot_group.addAction(self.individual_tab) overplot_group.addAction(self.latest_live) overplot_group.addAction(self.fixed) for action in overplot_group.actions(): action.setCheckable(True) overplot_group.setExclusive(True) self.off.setChecked(True) overplot_menu = menuBar().addMenu('&Streaming') overplot_menu.addActions(overplot_group.actions()) self.off.triggered.connect(self.disable_live) self.individual_tab.triggered.connect(partial(self.set_overplot_state, OverPlotState.individual_tab)) self.latest_live.triggered.connect(partial(self.set_overplot_state, OverPlotState.latest_live)) def set_overplot_target(): item, ok = QInputDialog.getItem( self, "Select Tab", "Tab", tuple(self._title_to_tab), 0, False) if not ok: # Abort and fallback to Off. Would be better to fall back to # previous state (which could be latest_live) but it's not # clear how to know what that state was. self.off.setChecked(True) return self.set_overplot_state(OverPlotState.fixed) self._overplot_target = item self.fixed.triggered.connect(set_overplot_target)
def _make_sort_button(self): """ Make the sort button, with separate groups for ascending and descending, sorting by name or last shown :return: The sort menu button """ sort_button = QPushButton("Sort") sort_menu = QMenu() ascending_action = QAction("Ascending", sort_menu, checkable=True) ascending_action.setChecked(True) ascending_action.toggled.connect(self.presenter.set_sort_order) descending_action = QAction("Descending", sort_menu, checkable=True) order_group = QActionGroup(sort_menu) order_group.addAction(ascending_action) order_group.addAction(descending_action) number_action = QAction("Number", sort_menu, checkable=True) number_action.setChecked(True) number_action.toggled.connect( lambda: self.presenter.set_sort_type(Column.Number)) name_action = QAction("Name", sort_menu, checkable=True) name_action.toggled.connect( lambda: self.presenter.set_sort_type(Column.Name)) last_active_action = QAction("Last Active", sort_menu, checkable=True) last_active_action.toggled.connect( lambda: self.presenter.set_sort_type(Column.LastActive)) sort_type_group = QActionGroup(sort_menu) sort_type_group.addAction(number_action) sort_type_group.addAction(name_action) sort_type_group.addAction(last_active_action) sort_menu.addAction(ascending_action) sort_menu.addAction(descending_action) sort_menu.addSeparator() sort_menu.addAction(number_action) sort_menu.addAction(name_action) sort_menu.addAction(last_active_action) sort_button.setMenu(sort_menu) return sort_button
def _add_plot_type_option_menu(self, menu, ax): plot_type_menu = QMenu("Plot Type", menu) plot_type_action_group = QActionGroup(plot_type_menu) standard = plot_type_menu.addAction( "1D", lambda: self._change_plot_type( ax, plot_type_action_group.checkedAction())) waterfall = plot_type_menu.addAction( "Waterfall", lambda: self._change_plot_type( ax, plot_type_action_group.checkedAction())) for action in [waterfall, standard]: plot_type_action_group.addAction(action) action.setCheckable(True) if ax.is_waterfall(): waterfall.setChecked(True) else: standard.setChecked(True) menu.addMenu(plot_type_menu)
def _make_sort_button(self): """ Make the sort button, with separate groups for ascending and descending, sorting by name or last shown :return: The sort menu button """ sort_button = QPushButton("Sort") sort_menu = QMenu() ascending_action = QAction("Ascending", sort_menu, checkable=True) ascending_action.setChecked(True) ascending_action.toggled.connect(self.presenter.set_sort_order) descending_action = QAction("Descending", sort_menu, checkable=True) order_group = QActionGroup(sort_menu) order_group.addAction(ascending_action) order_group.addAction(descending_action) number_action = QAction("Number", sort_menu, checkable=True) number_action.setChecked(True) number_action.toggled.connect(lambda: self.presenter.set_sort_type(Column.Number)) name_action = QAction("Name", sort_menu, checkable=True) name_action.toggled.connect(lambda: self.presenter.set_sort_type(Column.Name)) last_active_action = QAction("Last Active", sort_menu, checkable=True) last_active_action.toggled.connect(lambda: self.presenter.set_sort_type(Column.LastActive)) sort_type_group = QActionGroup(sort_menu) sort_type_group.addAction(number_action) sort_type_group.addAction(name_action) sort_type_group.addAction(last_active_action) sort_menu.addAction(ascending_action) sort_menu.addAction(descending_action) sort_menu.addSeparator() sort_menu.addAction(number_action) sort_menu.addAction(name_action) sort_menu.addAction(last_active_action) sort_button.setMenu(sort_menu) return sort_button
def generateContextMenu(self): """ Generate the window's context menu. This first calls the base class's context menu generator and then extends it with the filtering options. """ qmenu = super(MessageDisplay, self).generateContextMenu() filter_menu = qmenu.addMenu("&View") framework_action = QAction('Mantid Log Output', filter_menu) framework_action.triggered.connect(self.toggle_filter_framework_output) framework_action.setCheckable(True) framework_action.setChecked(self.showFrameworkOutput()) filter_menu.addAction(framework_action) filter_menu.addSeparator() actions_to_group = [] active_script_action = QAction("Active Tab Output", filter_menu) active_script_action.triggered.connect(self.show_active_script) actions_to_group.append(active_script_action) all_script_action = QAction('All Script Output', filter_menu) all_script_action.triggered.connect(self.show_all_scripts) actions_to_group.append(all_script_action) hide_all_script_action = QAction("Hide All Script Output", filter_menu) hide_all_script_action.triggered.connect(self.hide_all_scripts) actions_to_group.append(hide_all_script_action) action_group = QActionGroup(filter_menu) for action in actions_to_group: action_group.addAction(action) filter_menu.addAction(action) action.setCheckable(True) if self.showAllScriptOutput(): all_script_action.setChecked(True) elif self.showActiveScriptOutput(): active_script_action.setChecked(True) else: hide_all_script_action.setChecked(True) return qmenu
def setup(self): self.wrap_action = self.create_action( name=HelpWidgetActions.ToggleWrap, text=_("Wrap lines"), toggled=True, initial=self.get_conf('wrap'), option='wrap' ) self.copy_action = self.create_action( name=HelpWidgetActions.CopyAction, text=_("Copy"), triggered=lambda value: self.plain_text.copy(), register_shortcut=False, ) self.select_all_action = self.create_action( name=HelpWidgetActions.SelectAll, text=_("Select All"), triggered=lambda value: self.plain_text.select_all(), register_shortcut=False, ) self.auto_import_action = self.create_action( name=HelpWidgetActions.ToggleAutomaticImport, text=_("Automatic import"), toggled=True, initial=self.get_conf('automatic_import'), option='automatic_import' ) self.show_source_action = self.create_action( name=HelpWidgetActions.ToggleShowSource, text=_("Show Source"), toggled=True, option='show_source' ) self.rich_text_action = self.create_action( name=HelpWidgetActions.ToggleRichMode, text=_("Rich Text"), toggled=True, initial=self.get_conf('rich_mode'), option='rich_mode' ) self.plain_text_action = self.create_action( name=HelpWidgetActions.TogglePlainMode, text=_("Plain Text"), toggled=True, initial=self.get_conf('plain_mode'), option='plain_mode' ) self.locked_action = self.create_action( name=HelpWidgetActions.ToggleLocked, text=_("Lock/Unlock"), toggled=True, icon=self.create_icon('lock_open'), initial=self.get_conf('locked'), option='locked' ) self.home_action = self.create_action( name=HelpWidgetActions.Home, text=_("Home"), triggered=self.show_intro_message, icon=self.create_icon('home'), ) # Add the help actions to an exclusive QActionGroup help_actions = QActionGroup(self) help_actions.setExclusive(True) help_actions.addAction(self.plain_text_action) help_actions.addAction(self.rich_text_action) # Menu menu = self.get_options_menu() for item in [self.rich_text_action, self.plain_text_action, self.show_source_action]: self.add_item_to_menu( item, menu=menu, section=HelpWidgetOptionsMenuSections.Display, ) self.add_item_to_menu( self.auto_import_action, menu=menu, section=HelpWidgetOptionsMenuSections.Other, ) # Plain text menu self._plain_text_context_menu = self.create_menu( "plain_text_context_menu") self.add_item_to_menu( self.copy_action, self._plain_text_context_menu, section="copy_section", ) self.add_item_to_menu( self.select_all_action, self._plain_text_context_menu, section="select_section", ) self.add_item_to_menu( self.wrap_action, self._plain_text_context_menu, section="wrap_section", ) # Toolbar toolbar = self.get_main_toolbar() for item in [self.source_label, self.source_combo, self.object_label, self.object_combo, self.object_edit, self.home_action, self.locked_action]: self.add_item_to_toolbar( item, toolbar=toolbar, section=HelpWidgetMainToolbarSections.Main, ) self.source_changed() self.switch_to_rich_text() self.show_intro_message() # Signals self.plain_text.sig_custom_context_menu_requested.connect( self._show_plain_text_context_menu)
def setup(self, options): self.wrap_action = self.create_action( name=HelpWidgetActions.ToggleWrap, text=_("Wrap lines"), toggled=lambda value: self.set_option('wrap', value), initial=self.get_option('wrap'), ) self.auto_import_action = self.create_action( name=HelpWidgetActions.ToggleAutomaticImport, text=_("Automatic import"), toggled=lambda value: self.set_option('automatic_import', value), initial=self.get_option('automatic_import'), ) self.show_source_action = self.create_action( name=HelpWidgetActions.ToggleShowSource, text=_("Show Source"), toggled=lambda value: self.set_option('show_source', value), ) self.rich_text_action = self.create_action( name=HelpWidgetActions.ToggleRichMode, text=_("Rich Text"), toggled=lambda value: self.set_option('rich_mode', value), initial=self.get_option('rich_mode'), ) self.plain_text_action = self.create_action( name=HelpWidgetActions.TogglePlainMode, text=_("Plain Text"), toggled=lambda value: self.set_option('plain_mode', value), initial=self.get_option('plain_mode'), ) self.locked_action = self.create_action( name=HelpWidgetActions.ToggleLocked, text=_("Lock/Unlock"), toggled=lambda value: self.set_option('locked', value), icon=self.create_icon('lock_open'), initial=self.get_option('locked'), ) # TODO: Temporal while code editor migrates to SpyderMixin self.plain_text.set_wrap_action(self.wrap_action) # Add the help actions to an exclusive QActionGroup help_actions = QActionGroup(self) help_actions.setExclusive(True) help_actions.addAction(self.plain_text_action) help_actions.addAction(self.rich_text_action) # Menu menu = self.get_options_menu() for item in [self.rich_text_action, self.plain_text_action, self.show_source_action]: self.add_item_to_menu( item, menu=menu, section=HelpWidgetOptionsMenuSections.Display, ) self.add_item_to_menu( self.auto_import_action, menu=menu, section=HelpWidgetOptionsMenuSections.Other, ) # Toolbar toolbar = self.get_main_toolbar() for item in [self.source_label, self.source_combo, self.object_label, self.object_combo, self.object_edit, self.locked_action]: self.add_item_to_toolbar( item, toolbar=toolbar, section=HelpWidgetMainToolBarSections.Main, ) self.source_changed() self.switch_to_rich_text() self.show_intro_message()
def __init__(self, parent): if PYQT5: SpyderPluginWidget.__init__(self, parent, main = parent) else: SpyderPluginWidget.__init__(self, parent) self.internal_shell = None # Initialize plugin self.initialize_plugin() self.no_doc_string = _("No further documentation available") self._last_console_cb = None self._last_editor_cb = None self.plain_text = PlainText(self) self.rich_text = RichText(self) color_scheme = self.get_color_scheme() self.set_plain_text_font(self.get_plugin_font(), color_scheme) self.plain_text.editor.toggle_wrap_mode(self.get_option('wrap')) # Add entries to read-only editor context-menu self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked(self.get_option('wrap')) self.plain_text.editor.readonly_menu.addSeparator() add_actions(self.plain_text.editor.readonly_menu, (self.wrap_action,)) self.set_rich_text_font(self.get_plugin_font('rich_text')) self.shell = None self.external_console = None # locked = disable link with Console self.locked = False self._last_texts = [None, None] self._last_editor_doc = None # Object name layout_edit = QHBoxLayout() layout_edit.setContentsMargins(0, 0, 0, 0) txt = _("Source") if sys.platform == 'darwin': source_label = QLabel(" " + txt) else: source_label = QLabel(txt) layout_edit.addWidget(source_label) self.source_combo = QComboBox(self) self.source_combo.addItems([_("Console"), _("Editor")]) self.source_combo.currentIndexChanged.connect(self.source_changed) if (not programs.is_module_installed('rope') and not programs.is_module_installed('jedi', '>=0.8.1')): self.source_combo.hide() source_label.hide() layout_edit.addWidget(self.source_combo) layout_edit.addSpacing(10) layout_edit.addWidget(QLabel(_("Object"))) self.combo = ObjectComboBox(self) layout_edit.addWidget(self.combo) self.object_edit = QLineEdit(self) self.object_edit.setReadOnly(True) layout_edit.addWidget(self.object_edit) self.combo.setMaxCount(self.get_option('max_history_entries')) self.combo.addItems( self.load_history() ) self.combo.setItemText(0, '') self.combo.valid.connect(lambda valid: self.force_refresh()) # Plain text docstring option self.docstring = True self.rich_help = self.get_option('rich_mode', True) self.plain_text_action = create_action(self, _("Plain Text"), toggled=self.toggle_plain_text) # Source code option self.show_source_action = create_action(self, _("Show Source"), toggled=self.toggle_show_source) # Rich text option self.rich_text_action = create_action(self, _("Rich Text"), toggled=self.toggle_rich_text) # Add the help actions to an exclusive QActionGroup help_actions = QActionGroup(self) help_actions.setExclusive(True) help_actions.addAction(self.plain_text_action) help_actions.addAction(self.rich_text_action) # Automatic import option self.auto_import_action = create_action(self, _("Automatic import"), toggled=self.toggle_auto_import) auto_import_state = self.get_option('automatic_import') self.auto_import_action.setChecked(auto_import_state) # Lock checkbox self.locked_button = create_toolbutton(self, triggered=self.toggle_locked) layout_edit.addWidget(self.locked_button) self._update_lock_icon() # Option menu options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, [self.rich_text_action, self.plain_text_action, self.show_source_action, None, self.auto_import_action]) options_button.setMenu(menu) layout_edit.addWidget(options_button) if self.rich_help: self.switch_to_rich_text() else: self.switch_to_plain_text() self.plain_text_action.setChecked(not self.rich_help) self.rich_text_action.setChecked(self.rich_help) self.source_changed() # Main layout layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(layout_edit) layout.addWidget(self.plain_text) layout.addWidget(self.rich_text) self.setLayout(layout) # Add worker thread for handling rich text rendering self._sphinx_thread = SphinxThread( html_text_no_doc=warning(self.no_doc_string)) self._sphinx_thread.html_ready.connect( self._on_sphinx_thread_html_ready) self._sphinx_thread.error_msg.connect(self._on_sphinx_thread_error_msg) # Handle internal and external links view = self.rich_text.webview if not WEBENGINE: view.page().setLinkDelegationPolicy(QWebEnginePage.DelegateAllLinks) view.linkClicked.connect(self.handle_link_clicks) self._starting_up = True
def __init__(self, model, *args, **kwargs): super(PlotWindow, self).__init__(*args, **kwargs) # Hide the icon in the title bar self.setWindowIcon(qta.icon('fa.circle', opacity=0)) # The central widget of the sub window will be a main window so that it # can support having tab bars self._central_widget = QMainWindow() self.setWidget(self._central_widget) loadUi(os.path.join(os.path.dirname(__file__), "ui", "plot_window.ui"), self._central_widget) # The central widget of the main window widget will be the plot self._model = model self._current_item_index = None self._plot_widget = PlotWidget(model=self._model) self._plot_widget.plotItem.setMenuEnabled(False) self._central_widget.setCentralWidget(self._plot_widget) # Setup action group for interaction modes mode_group = QActionGroup(self.tool_bar) mode_group.addAction(self._central_widget.pan_mode_action) self._central_widget.pan_mode_action.setChecked(True) mode_group.addAction(self._central_widget.zoom_mode_action) def _toggle_mode(state): view_state = self.plot_widget.plotItem.getViewBox().state.copy() view_state.update({ 'mouseMode': pg.ViewBox.RectMode if state else pg.ViewBox.PanMode }) self.plot_widget.plotItem.getViewBox().setState(view_state) # Setup plot settings options menu self.plot_settings_button = self.tool_bar.widgetForAction( self._central_widget.plot_settings_action) self.plot_settings_button.setPopupMode(QToolButton.InstantPopup) self.plot_settings_menu = QMenu(self.plot_settings_button) self.plot_settings_button.setMenu(self.plot_settings_menu) self.color_change_action = QAction("Line Color") self.plot_settings_menu.addAction(self.color_change_action) self.line_width_menu = QMenu("Line Widths") self.plot_settings_menu.addMenu(self.line_width_menu) # Setup the line width plot setting options for i in range(1, 4): act = QAction(str(i), self.line_width_menu) self.line_width_menu.addAction(act) act.triggered.connect( lambda *args, size=i: self._on_change_width(size)) # Setup connections self._central_widget.pan_mode_action.triggered.connect( lambda: _toggle_mode(False)) self._central_widget.zoom_mode_action.triggered.connect( lambda: _toggle_mode(True)) self._central_widget.linear_region_action.triggered.connect( self.plot_widget._on_add_linear_region) self._central_widget.remove_region_action.triggered.connect( self.plot_widget._on_remove_linear_region) self.color_change_action.triggered.connect(self._on_change_color) self._central_widget.export_plot_action.triggered.connect( self._on_export_plot) self._central_widget.reset_view_action.triggered.connect( lambda: self._on_reset_view())
def setup_menu_actions(self): # flow designs light_themes_menu = QMenu('light') for d in self.session.design.flow_themes: design_action = QAction(d.name, self) if d.type_ == 'dark': self.ui.menuFlow_Design_Style.addAction(design_action) else: light_themes_menu.addAction(design_action) design_action.triggered.connect(self.on_design_action_triggered) self.flow_view_theme_actions.append(design_action) self.ui.menuFlow_Design_Style.addMenu(light_themes_menu) self.ui.actionImport_Nodes.triggered.connect(self.on_import_nodes_triggered) self.ui.actionSave_Project.triggered.connect(self.on_save_project_triggered) self.ui.actionEnableInfoMessages.triggered.connect(self.on_enable_info_msgs_triggered) self.ui.actionDisableInfoMessages.triggered.connect(self.on_disable_info_msgs_triggered) self.ui.actionSave_Pic_Viewport.triggered.connect(self.on_save_scene_pic_viewport_triggered) self.ui.actionSave_Pic_Whole_Scene_scaled.triggered.connect(self.on_save_scene_pic_whole_triggered) # performance mode self.ac_perf_mode_fast = QAction('Fast', self) self.ac_perf_mode_fast.setCheckable(True) self.ac_perf_mode_pretty = QAction('Pretty', self) self.ac_perf_mode_pretty.setCheckable(True) perf_mode_ag = QActionGroup(self) perf_mode_ag.addAction(self.ac_perf_mode_fast) perf_mode_ag.addAction(self.ac_perf_mode_pretty) self.ac_perf_mode_fast.setChecked(self.session.design.performance_mode == 'fast') self.ac_perf_mode_pretty.setChecked(self.session.design.performance_mode == 'pretty') perf_mode_ag.triggered.connect(self.on_performance_mode_changed) perf_menu = QMenu('Performance Mode', self) perf_menu.addAction(self.ac_perf_mode_fast) perf_menu.addAction(self.ac_perf_mode_pretty) self.ui.menuView.addMenu(perf_menu) # animations self.ac_anims_active = QAction('Enabled', self) self.ac_anims_active.setCheckable(True) self.ac_anims_inactive = QAction('Disabled', self) self.ac_anims_inactive.setCheckable(True) anims_ag = QActionGroup(self) anims_ag.addAction(self.ac_anims_active) anims_ag.addAction(self.ac_anims_inactive) self.ac_anims_active.setChecked(self.session.design.animations_enabled) self.ac_anims_inactive.setChecked(not self.session.design.animations_enabled) anims_ag.triggered.connect(self.on_animation_enabling_changed) animations_menu = QMenu('Animations', self) animations_menu.addAction(self.ac_anims_active) animations_menu.addAction(self.ac_anims_inactive) self.ui.menuView.addMenu(animations_menu)
def __init__(self, parent=None, css_path=CSS_PATH): SpyderPluginWidget.__init__(self, parent) self.internal_shell = None self.console = None self.css_path = css_path self.no_doc_string = _("No documentation available") self._last_console_cb = None self._last_editor_cb = None self.plain_text = PlainText(self) self.rich_text = RichText(self) color_scheme = self.get_color_scheme() self.set_plain_text_font(self.get_font(), color_scheme) self.plain_text.editor.toggle_wrap_mode(self.get_option('wrap')) # Add entries to read-only editor context-menu self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked(self.get_option('wrap')) self.plain_text.editor.readonly_menu.addSeparator() add_actions(self.plain_text.editor.readonly_menu, (self.wrap_action,)) self.set_rich_text_font(self.get_font(rich_text=True)) self.shell = None # locked = disable link with Console self.locked = False self._last_texts = [None, None] self._last_editor_doc = None # Object name layout_edit = QHBoxLayout() layout_edit.setContentsMargins(0, 0, 0, 0) txt = _("Source") if sys.platform == 'darwin': source_label = QLabel(" " + txt) else: source_label = QLabel(txt) layout_edit.addWidget(source_label) self.source_combo = QComboBox(self) self.source_combo.addItems([_("Console"), _("Editor")]) self.source_combo.currentIndexChanged.connect(self.source_changed) if (not programs.is_module_installed('rope') and not programs.is_module_installed('jedi', '>=0.11.0')): self.source_combo.hide() source_label.hide() layout_edit.addWidget(self.source_combo) layout_edit.addSpacing(10) layout_edit.addWidget(QLabel(_("Object"))) self.combo = ObjectComboBox(self) layout_edit.addWidget(self.combo) self.object_edit = QLineEdit(self) self.object_edit.setReadOnly(True) layout_edit.addWidget(self.object_edit) self.combo.setMaxCount(self.get_option('max_history_entries')) self.combo.addItems( self.load_history() ) self.combo.setItemText(0, '') self.combo.valid.connect(self.force_refresh) # Plain text docstring option self.docstring = True self.rich_help = self.get_option('rich_mode', True) self.plain_text_action = create_action(self, _("Plain Text"), toggled=self.toggle_plain_text) # Source code option self.show_source_action = create_action(self, _("Show Source"), toggled=self.toggle_show_source) # Rich text option self.rich_text_action = create_action(self, _("Rich Text"), toggled=self.toggle_rich_text) # Add the help actions to an exclusive QActionGroup help_actions = QActionGroup(self) help_actions.setExclusive(True) help_actions.addAction(self.plain_text_action) help_actions.addAction(self.rich_text_action) # Automatic import option self.auto_import_action = create_action(self, _("Automatic import"), toggled=self.toggle_auto_import) auto_import_state = self.get_option('automatic_import') self.auto_import_action.setChecked(auto_import_state) # Lock checkbox self.locked_button = create_toolbutton(self, triggered=self.toggle_locked) layout_edit.addWidget(self.locked_button) self._update_lock_icon() # Option menu layout_edit.addWidget(self.options_button) if self.rich_help: self.switch_to_rich_text() else: self.switch_to_plain_text() self.plain_text_action.setChecked(not self.rich_help) self.rich_text_action.setChecked(self.rich_help) self.source_changed() # Main layout layout = create_plugin_layout(layout_edit) # we have two main widgets, but only one of them is shown at a time layout.addWidget(self.plain_text) layout.addWidget(self.rich_text) self.setLayout(layout) # Add worker thread for handling rich text rendering self._sphinx_thread = SphinxThread( html_text_no_doc=warning(self.no_doc_string, css_path=self.css_path), css_path=self.css_path) self._sphinx_thread.html_ready.connect( self._on_sphinx_thread_html_ready) self._sphinx_thread.error_msg.connect(self._on_sphinx_thread_error_msg) # Handle internal and external links view = self.rich_text.webview if not WEBENGINE: view.page().setLinkDelegationPolicy(QWebEnginePage.DelegateAllLinks) view.linkClicked.connect(self.handle_link_clicks) self._starting_up = True
class PyDMImageView(ImageView, PyDMWidget, PyDMColorMap, ReadingOrder): """ A PyQtGraph ImageView with support for Channels and more from PyDM. If there is no :attr:`channelWidth` it is possible to define the width of the image with the :attr:`width` property. The :attr:`normalizeData` property defines if the colors of the images are relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to the minimum and maximum values of the image. Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new image is rendered in the widget. Parameters ---------- parent : QWidget The parent widget for the Label image_channel : str, optional The channel to be used by the widget for the image data. width_channel : str, optional The channel to be used by the widget to receive the image width information """ ReadingOrder = ReadingOrder Q_ENUMS(ReadingOrder) Q_ENUMS(PyDMColorMap) color_maps = cmaps def __init__(self, parent=None, image_channel=None, width_channel=None): """Initialize widget.""" # Set the default colormap. self._colormap = PyDMColorMap.Inferno self._cm_colors = None self._imagechannel = None self._widthchannel = None self.image_waveform = np.zeros(0) self._image_width = 0 self._normalize_data = False self._auto_downsample = True self._show_axes = False # Set default reading order of numpy array data to Fortranlike. self._reading_order = ReadingOrder.Fortranlike self._redraw_rate = 30 # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 plot_item = PlotItem() ImageView.__init__(self, parent, view=plot_item) PyDMWidget.__init__(self) self._channels = [None, None] self.thread = None self.axes = dict({'t': None, "x": 0, "y": 1, "c": None}) self.showAxes = self._show_axes # Hide some itens of the widget. self.ui.histogram.hide() self.getImageItem().sigImageChanged.disconnect( self.ui.histogram.imageChanged) self.ui.roiBtn.hide() self.ui.menuBtn.hide() # Make a right-click menu for changing the color map. self.cm_group = QActionGroup(self) self.cmap_for_action = {} for cm in self.color_maps: action = self.cm_group.addAction(cmap_names[cm]) action.setCheckable(True) self.cmap_for_action[action] = cm self.colorMap = self._colormap # Setup the redraw timer. self.needs_redraw = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawImage) self.maxRedrawRate = self._redraw_rate self.newImageSignal = self.getImageItem().sigImageChanged # Set live channels if requested on initialization if image_channel: self.imageChannel = image_channel or '' if width_channel: self.widthChannel = width_channel or '' @Property(str, designable=False) def channel(self): return @channel.setter def channel(self, ch): if not ch: return logger.info("Use the imageChannel property with the ImageView widget.") return def widget_ctx_menu(self): """ Fetch the Widget specific context menu. It will be populated with additional tools by `assemble_tools_menu`. Returns ------- QMenu or None If the return of this method is None a new QMenu will be created by `assemble_tools_menu`. """ self.menu = ViewBoxMenu(self.getView().getViewBox()) cm_menu = self.menu.addMenu("Color Map") for act in self.cmap_for_action.keys(): cm_menu.addAction(act) cm_menu.triggered.connect(self._changeColorMap) return self.menu def _changeColorMap(self, action): """ Method invoked by the colormap Action Menu. Changes the current colormap used to render the image. Parameters ---------- action : QAction """ self.colorMap = self.cmap_for_action[action] @Property(float) def colorMapMin(self): """ Minimum value for the colormap. Returns ------- float """ return self.cm_min @colorMapMin.setter @Slot(float) def colorMapMin(self, new_min): """ Set the minimum value for the colormap. Parameters ---------- new_min : float """ if self.cm_min != new_min: self.cm_min = new_min if self.cm_min > self.cm_max: self.cm_max = self.cm_min @Property(float) def colorMapMax(self): """ Maximum value for the colormap. Returns ------- float """ return self.cm_max @colorMapMax.setter @Slot(float) def colorMapMax(self, new_max): """ Set the maximum value for the colormap. Parameters ---------- new_max : float """ if self.cm_max != new_max: self.cm_max = new_max if self.cm_max < self.cm_min: self.cm_min = self.cm_max def setColorMapLimits(self, mn, mx): """ Set the limit values for the colormap. Parameters ---------- mn : int The lower limit mx : int The upper limit """ if mn >= mx: return self.cm_max = mx self.cm_min = mn @Property(PyDMColorMap) def colorMap(self): """ Return the color map used by the ImageView. Returns ------- PyDMColorMap """ return self._colormap @colorMap.setter def colorMap(self, new_cmap): """ Set the color map used by the ImageView. Parameters ------- new_cmap : PyDMColorMap """ self._colormap = new_cmap self._cm_colors = self.color_maps[new_cmap] self.setColorMap() for action in self.cm_group.actions(): if self.cmap_for_action[action] == self._colormap: action.setChecked(True) else: action.setChecked(False) def setColorMap(self, cmap=None): """ Update the image colormap. Parameters ---------- cmap : ColorMap """ if not cmap: if not self._cm_colors.any(): return # Take default values pos = np.linspace(0.0, 1.0, num=len(self._cm_colors)) cmap = ColorMap(pos, self._cm_colors) self.getView().getViewBox().setBackgroundColor(cmap.map(0)) lut = cmap.getLookupTable(0.0, 1.0, alpha=False) self.getImageItem().setLookupTable(lut) @Slot(bool) def image_connection_state_changed(self, conn): """ Callback invoked when the Image Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ if conn: self.redraw_timer.start() else: self.redraw_timer.stop() @Slot(np.ndarray) def image_value_changed(self, new_image): """ Callback invoked when the Image Channel value is changed. We try to do as little as possible in this method, because it gets called every time the image channel updates, which might be extremely often. Basically just store the data, and set a flag requesting that the image be redrawn. Parameters ---------- new_image : np.ndarray The new image data. This can be a flat 1D array, or a 2D array. """ if new_image is None or new_image.size == 0: return logging.debug("ImageView Received New Image - Needs Redraw -> True") self.image_waveform = new_image self.needs_redraw = True @Slot(int) def image_width_changed(self, new_width): """ Callback invoked when the Image Width Channel value is changed. Parameters ---------- new_width : int The new image width """ if new_width is None: return self._image_width = int(new_width) def process_image(self, image): """ Boilerplate method to be used by applications in order to add calculations and also modify the image before it is displayed at the widget. .. warning:: This code runs in a separated QThread so it **MUST** not try to write to QWidgets. Parameters ---------- image : np.ndarray The Image Data as a 2D numpy array Returns ------- np.ndarray The Image Data as a 2D numpy array after processing. """ return image def redrawImage(self): """ Set the image data into the ImageItem, if needed. If necessary, reshape the image to 2D first. """ if self.thread is not None and not self.thread.isFinished(): logger.warning( "Image processing has taken longer than the refresh rate.") return self.thread = ImageUpdateThread(self) self.thread.updateSignal.connect(self.__updateDisplay) logging.debug("ImageView RedrawImage Thread Launched") self.thread.start() @Slot(list) def __updateDisplay(self, data): logging.debug("ImageView Update Display with new image") mini, maxi = data[0], data[1] img = data[2] self.getImageItem().setLevels([mini, maxi]) self.getImageItem().setImage( img, autoLevels=False, autoDownsample=self.autoDownsample) @Property(bool) def autoDownsample(self): """ Return if we should or not apply the autoDownsample option to PyQtGraph. Return ------ bool """ return self._auto_downsample @autoDownsample.setter def autoDownsample(self, new_value): """ Whether we should or not apply the autoDownsample option to PyQtGraph. Parameters ---------- new_value: bool """ if new_value != self._auto_downsample: self._auto_downsample = new_value @Property(int) def imageWidth(self): """ Return the width of the image. Return ------ int """ return self._image_width @imageWidth.setter def imageWidth(self, new_width): """ Set the width of the image. Can be overridden by :attr:`widthChannel`. Parameters ---------- new_width: int """ if (self._image_width != int(new_width) and (self._widthchannel is None or self._widthchannel == '')): self._image_width = int(new_width) @Property(bool) def normalizeData(self): """ Return True if the colors are relative to data maximum and minimum. Returns ------- bool """ return self._normalize_data @normalizeData.setter @Slot(bool) def normalizeData(self, new_norm): """ Define if the colors are relative to minimum and maximum of the data. Parameters ---------- new_norm: bool """ if self._normalize_data != new_norm: self._normalize_data = new_norm @Property(ReadingOrder) def readingOrder(self): """ Return the reading order of the :attr:`imageChannel` array. Returns ------- ReadingOrder """ return self._reading_order @readingOrder.setter def readingOrder(self, new_order): """ Set reading order of the :attr:`imageChannel` array. Parameters ---------- new_order: ReadingOrder """ if self._reading_order != new_order: self._reading_order = new_order def keyPressEvent(self, ev): """Handle keypress events.""" return @Property(str) def imageChannel(self): """ The channel address in use for the image data . Returns ------- str Channel address """ if self._imagechannel: return str(self._imagechannel.address) else: return '' @imageChannel.setter def imageChannel(self, value): """ The channel address in use for the image data . Parameters ---------- value : str Channel address """ if self._imagechannel != value: # Disconnect old channel if self._imagechannel: self._imagechannel.disconnect() # Create and connect new channel self._imagechannel = PyDMChannel( address=value, connection_slot=self.image_connection_state_changed, value_slot=self.image_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[0] = self._imagechannel self._imagechannel.connect() @Property(str) def widthChannel(self): """ The channel address in use for the image width . Returns ------- str Channel address """ if self._widthchannel: return str(self._widthchannel.address) else: return '' @widthChannel.setter def widthChannel(self, value): """ The channel address in use for the image width . Parameters ---------- value : str Channel address """ if self._widthchannel != value: # Disconnect old channel if self._widthchannel: self._widthchannel.disconnect() # Create and connect new channel self._widthchannel = PyDMChannel( address=value, connection_slot=self.connectionStateChanged, value_slot=self.image_width_changed, severity_slot=self.alarmSeverityChanged) self._channels[1] = self._widthchannel self._widthchannel.connect() def channels(self): """ Return the channels being used for this Widget. Returns ------- channels : list List of PyDMChannel objects """ return self._channels def channels_for_tools(self): """Return channels for tools.""" return [self._imagechannel] @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000)) @Property(bool) def showAxes(self): """ Whether or not axes should be shown on the widget. """ return self._show_axes @showAxes.setter def showAxes(self, show): self._show_axes = show self.getView().showAxis('left', show=show) self.getView().showAxis('bottom', show=show) @Property(float) def scaleXAxis(self): """ Sets the scale for the X Axis. For example, if your image has 100 pixels per millimeter, you can set xAxisScale to 1/100 = 0.01 to make the X Axis report in millimeter units. """ # protect against access to not yet initialized view if hasattr(self, 'view'): return self.getView().getAxis('bottom').scale return None @scaleXAxis.setter def scaleXAxis(self, new_scale): self.getView().getAxis('bottom').setScale(new_scale) @Property(float) def scaleYAxis(self): """ Sets the scale for the Y Axis. For example, if your image has 100 pixels per millimeter, you can set yAxisScale to 1/100 = 0.01 to make the Y Axis report in millimeter units. """ # protect against access to not yet initialized view if hasattr(self, 'view'): return self.getView().getAxis('left').scale return None @scaleYAxis.setter def scaleYAxis(self, new_scale): self.getView().getAxis('left').setScale(new_scale)
def __init__(self, model, *args, **kwargs): super(PlotWindow, self).__init__(*args, **kwargs) # Hide the icon in the title bar self.setWindowIcon(qta.icon('fa.circle', opacity=0)) # The central widget of the sub window will be a main window so that it # can support having tab bars self._central_widget = QMainWindow() self.setWidget(self._central_widget) loadUi(os.path.join(os.path.dirname(__file__), "ui", "plot_window.ui"), self._central_widget) # The central widget of the main window widget will be the plot self._model = model self._current_item_index = None self._plot_widget = PlotWidget(model=self._model) self._plot_widget.plotItem.setMenuEnabled(False) self._central_widget.setCentralWidget(self._plot_widget) # Setup action group for interaction modes mode_group = QActionGroup(self.tool_bar) mode_group.addAction(self._central_widget.pan_mode_action) self._central_widget.pan_mode_action.setChecked(True) mode_group.addAction(self._central_widget.zoom_mode_action) def _toggle_mode(state): view_state = self.plot_widget.plotItem.getViewBox().state.copy() view_state.update({'mouseMode': pg.ViewBox.RectMode if state else pg.ViewBox.PanMode}) self.plot_widget.plotItem.getViewBox().setState(view_state) # Setup plot settings options menu self.plot_settings_button = self.tool_bar.widgetForAction( self._central_widget.plot_settings_action) self.plot_settings_button.setPopupMode(QToolButton.InstantPopup) self.plot_settings_menu = QMenu(self.plot_settings_button) self.plot_settings_button.setMenu(self.plot_settings_menu) self.color_change_action = QAction("Line Color") self.plot_settings_menu.addAction(self.color_change_action) self.line_width_menu = QMenu("Line Widths") self.plot_settings_menu.addMenu(self.line_width_menu) # Setup the line width plot setting options for i in range(1, 4): act = QAction(str(i), self.line_width_menu) self.line_width_menu.addAction(act) act.triggered.connect(lambda *args, size=i: self._on_change_width(size)) # Setup connections self._central_widget.pan_mode_action.triggered.connect( lambda: _toggle_mode(False)) self._central_widget.zoom_mode_action.triggered.connect( lambda: _toggle_mode(True)) self._central_widget.linear_region_action.triggered.connect( self.plot_widget._on_add_linear_region) self._central_widget.remove_region_action.triggered.connect( self.plot_widget._on_remove_linear_region) self.color_change_action.triggered.connect( self._on_change_color) self._central_widget.export_plot_action.triggered.connect( self._on_export_plot) self._central_widget.reset_view_action.triggered.connect( lambda: self._on_reset_view())
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap, ReadingOrder): """ A SpectrogramView with support for Channels and more from PyDM. If there is no :attr:`channelWidth` it is possible to define the width of the image with the :attr:`width` property. The :attr:`normalizeData` property defines if the colors of the images are relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to the minimum and maximum values of the image. Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new image is rendered in the widget. Parameters ---------- parent : QWidget The parent widget for the Label image_channel : str, optional The channel to be used by the widget for the image data. xaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Clike), and to set the xaxis values yaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Fortranlike), and to set the yaxis values background : QColor, optional QColor to set the background color of the GraphicsView """ Q_ENUMS(PyDMColorMap) Q_ENUMS(ReadingOrder) color_maps = cmaps def __init__(self, parent=None, image_channel=None, xaxis_channel=None, yaxis_channel=None, roioffsetx_channel=None, roioffsety_channel=None, roiwidth_channel=None, roiheight_channel=None, title='', background='w', image_width=0, image_height=0): """Initialize widget.""" GraphicsLayoutWidget.__init__(self, parent) PyDMWidget.__init__(self) self.thread = None self._imagechannel = None self._xaxischannel = None self._yaxischannel = None self._roioffsetxchannel = None self._roioffsetychannel = None self._roiwidthchannel = None self._roiheightchannel = None self._channels = 7 * [ None, ] self.image_waveform = np.zeros(0) self._image_width = image_width if not xaxis_channel else 0 self._image_height = image_height if not yaxis_channel else 0 self._roi_offsetx = 0 self._roi_offsety = 0 self._roi_width = 0 self._roi_height = 0 self._normalize_data = False self._auto_downsample = True self._last_yaxis_data = None self._last_xaxis_data = None self._auto_colorbar_lims = True self.format_tooltip = '{0:.4g}, {1:.4g}' # ViewBox and imageItem. self._view = ViewBox() self._image_item = ImageItem() self._view.addItem(self._image_item) # ROI self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0]) self.ROIColor = QColor('red') pen = mkPen() pen.setColor(QColor('transparent')) pen.setWidth(1) self.ROICurve.setPen(pen) self._view.addItem(self.ROICurve) # Axis. self.xaxis = AxisItem('bottom') self.xaxis.setPen(QColor(0, 0, 0)) if not xaxis_channel: self.xaxis.setVisible(False) self.yaxis = AxisItem('left') self.yaxis.setPen(QColor(0, 0, 0)) if not yaxis_channel: self.yaxis.setVisible(False) # Colorbar legend. self.colorbar = _GradientLegend() # Title. start_row = 0 if title: self.title = LabelItem(text=title, color='#000000') self.addItem(self.title, 0, 0, 1, 3) start_row = 1 # Set layout. self.addItem(self._view, start_row, 1) self.addItem(self.yaxis, start_row, 0) self.addItem(self.colorbar, start_row, 2) self.addItem(self.xaxis, start_row + 1, 1) self.setBackground(background) self.ci.layout.setColumnSpacing(0, 0) self.ci.layout.setRowSpacing(start_row, 0) # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 # Set default reading order of numpy array data to Clike. self._reading_order = ReadingOrder.Clike # Make a right-click menu for changing the color map. self.cm_group = QActionGroup(self) self.cmap_for_action = {} for cm in self.color_maps: action = self.cm_group.addAction(cmap_names[cm]) action.setCheckable(True) self.cmap_for_action[action] = cm # Set the default colormap. self._cm_colors = None self.colorMap = PyDMColorMap.Inferno # Setup the redraw timer. self.needs_redraw = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawImage) self._redraw_rate = 30 self.maxRedrawRate = self._redraw_rate self.newImageSignal = self._image_item.sigImageChanged # Set Channels. self.imageChannel = image_channel self.xAxisChannel = xaxis_channel self.yAxisChannel = yaxis_channel self.ROIOffsetXChannel = roioffsetx_channel self.ROIOffsetYChannel = roioffsety_channel self.ROIWidthChannel = roiwidth_channel self.ROIHeightChannel = roiheight_channel # --- Context menu --- def widget_ctx_menu(self): """ Fetch the Widget specific context menu. It will be populated with additional tools by `assemble_tools_menu`. Returns ------- QMenu or None If the return of this method is None a new QMenu will be created by `assemble_tools_menu`. """ self.menu = ViewBoxMenu(self._view) cm_menu = self.menu.addMenu("Color Map") for act in self.cmap_for_action.keys(): cm_menu.addAction(act) cm_menu.triggered.connect(self._changeColorMap) return self.menu # --- Colormap methods --- def _changeColorMap(self, action): """ Method invoked by the colormap Action Menu. Changes the current colormap used to render the image. Parameters ---------- action : QAction """ self.colorMap = self.cmap_for_action[action] @Property(float) def colorMapMin(self): """ Minimum value for the colormap. Returns ------- float """ return self.cm_min @colorMapMin.setter @Slot(float) def colorMapMin(self, new_min): """ Set the minimum value for the colormap. Parameters ---------- new_min : float """ if self.cm_min != new_min: self.cm_min = new_min if self.cm_min > self.cm_max: self.cm_max = self.cm_min @Property(float) def colorMapMax(self): """ Maximum value for the colormap. Returns ------- float """ return self.cm_max @colorMapMax.setter @Slot(float) def colorMapMax(self, new_max): """ Set the maximum value for the colormap. Parameters ---------- new_max : float """ if self.cm_max != new_max: self.cm_max = new_max if self.cm_max < self.cm_min: self.cm_min = self.cm_max def setColorMapLimits(self, mn, mx): """ Set the limit values for the colormap. Parameters ---------- mn : int The lower limit mx : int The upper limit """ if mn >= mx: return self.cm_max = mx self.cm_min = mn @Property(PyDMColorMap) def colorMap(self): """ Return the color map used by the SpectrogramView. Returns ------- PyDMColorMap """ return self._colormap @colorMap.setter def colorMap(self, new_cmap): """ Set the color map used by the SpectrogramView. Parameters ------- new_cmap : PyDMColorMap """ self._colormap = new_cmap self._cm_colors = self.color_maps[new_cmap] self.setColorMap() for action in self.cm_group.actions(): if self.cmap_for_action[action] == self._colormap: action.setChecked(True) else: action.setChecked(False) def setColorMap(self, cmap=None): """ Update the image colormap. Parameters ---------- cmap : ColorMap """ if not cmap: if not self._cm_colors.any(): return # Take default values pos = np.linspace(0.0, 1.0, num=len(self._cm_colors)) cmap = ColorMap(pos, self._cm_colors) self._view.setBackgroundColor(cmap.map(0)) lut = cmap.getLookupTable(0.0, 1.0, alpha=False) self.colorbar.setIntColorScale(colors=lut) self._image_item.setLookupTable(lut) # --- Connection Slots --- @Slot(bool) def image_connection_state_changed(self, conn): """ Callback invoked when the Image Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ if conn: self.redraw_timer.start() else: self.redraw_timer.stop() @Slot(bool) def yaxis_connection_state_changed(self, connected): """ Callback invoked when the TimeAxis Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ self._timeaxis_connected = connected @Slot(bool) def roioffsetx_connection_state_changed(self, conn): """ Run when the ROIOffsetX Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsetx = 0 @Slot(bool) def roioffsety_connection_state_changed(self, conn): """ Run when the ROIOffsetY Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsety = 0 @Slot(bool) def roiwidth_connection_state_changed(self, conn): """ Run when the ROIWidth Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_width = 0 @Slot(bool) def roiheight_connection_state_changed(self, conn): """ Run when the ROIHeight Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_height = 0 # --- Value Slots --- @Slot(np.ndarray) def image_value_changed(self, new_image): """ Callback invoked when the Image Channel value is changed. We try to do as little as possible in this method, because it gets called every time the image channel updates, which might be extremely often. Basically just store the data, and set a flag requesting that the image be redrawn. Parameters ---------- new_image : np.ndarray The new image data. This can be a flat 1D array, or a 2D array. """ if new_image is None or new_image.size == 0: return logging.debug("SpectrogramView Received New Image: Needs Redraw->True") self.image_waveform = new_image self.needs_redraw = True if not self._image_height and self._image_width: self._image_height = new_image.size / self._image_width elif not self._image_width and self._image_height: self._image_width = new_image.size / self._image_height @Slot(np.ndarray) @Slot(float) def xaxis_value_changed(self, new_array): """ Callback invoked when the Image Width Channel value is changed. Parameters ---------- new_array : np.ndarray The new x axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_xaxis_data = new_array if self._reading_order == self.Clike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(np.ndarray) @Slot(float) def yaxis_value_changed(self, new_array): """ Callback invoked when the TimeAxis Channel value is changed. Parameters ---------- new_array : np.array The new y axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_yaxis_data = new_array if self._reading_order == self.Fortranlike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(int) def roioffsetx_value_changed(self, new_offset): """ Run when the ROIOffsetX Channel value changes. Parameters ---------- new_offsetx : int The new image ROI horizontal offset """ if new_offset is None: return self._roi_offsetx = new_offset self.redrawROI() @Slot(int) def roioffsety_value_changed(self, new_offset): """ Run when the ROIOffsetY Channel value changes. Parameters ---------- new_offsety : int The new image ROI vertical offset """ if new_offset is None: return self._roi_offsety = new_offset self.redrawROI() @Slot(int) def roiwidth_value_changed(self, new_width): """ Run when the ROIWidth Channel value changes. Parameters ---------- new_width : int The new image ROI width """ if new_width is None: return self._roi_width = int(new_width) self.redrawROI() @Slot(int) def roiheight_value_changed(self, new_height): """ Run when the ROIHeight Channel value changes. Parameters ---------- new_height : int The new image ROI height """ if new_height is None: return self._roi_height = int(new_height) self.redrawROI() # --- Image update methods --- def process_image(self, image): """ Boilerplate method. To be used by applications in order to add calculations and also modify the image before it is displayed at the widget. .. warning:: This code runs in a separated QThread so it **MUST** not try to write to QWidgets. Parameters ---------- image : np.ndarray The Image Data as a 2D numpy array Returns ------- np.ndarray The Image Data as a 2D numpy array after processing. """ return image def redrawImage(self): """ Set the image data into the ImageItem, if needed. If necessary, reshape the image to 2D first. """ if self.thread is not None and not self.thread.isFinished(): logger.warning( "Image processing has taken longer than the refresh rate.") return self.thread = SpectrogramUpdateThread(self) self.thread.updateSignal.connect(self._updateDisplay) logging.debug("SpectrogramView RedrawImage Thread Launched") self.thread.start() @Slot(list) def _updateDisplay(self, data): logging.debug("SpectrogramView Update Display with new image") # Update axis if self._last_xaxis_data is not None: szx = self._last_xaxis_data.size xMin = self._last_xaxis_data.min() xMax = self._last_xaxis_data.max() else: szx = self.imageWidth if self.readingOrder == self.Clike \ else self.imageHeight xMin = 0 xMax = szx if self._last_yaxis_data is not None: szy = self._last_yaxis_data.size yMin = self._last_yaxis_data.min() yMax = self._last_yaxis_data.max() else: szy = self.imageHeight if self.readingOrder == self.Clike \ else self.imageWidth yMin = 0 yMax = szy self.xaxis.setRange(xMin, xMax) self.yaxis.setRange(yMin, yMax) self._view.setLimits(xMin=0, xMax=szx, yMin=0, yMax=szy, minXRange=szx, maxXRange=szx, minYRange=szy, maxYRange=szy) # Update image if self.autoSetColorbarLims: self.colorbar.setLimits(data) mini, maxi = data[0], data[1] img = data[2] self._image_item.setLevels([mini, maxi]) self._image_item.setImage(img, autoLevels=False, autoDownsample=self.autoDownsample) # ROI update methods def redrawROI(self): startx = self._roi_offsetx endx = self._roi_offsetx + self._roi_width starty = self._roi_offsety endy = self._roi_offsety + self._roi_height self.ROICurve.setData([startx, startx, endx, endx, startx], [starty, endy, endy, starty, starty]) def showROI(self, show): """Set ROI visibility.""" pen = mkPen() if show: pen.setColor(self.ROIColor) else: pen.setColor(QColor('transparent')) self.ROICurve.setPen(pen) # --- Properties --- @Property(bool) def autoDownsample(self): """ Return if we should or not apply the autoDownsample option. Return ------ bool """ return self._auto_downsample @autoDownsample.setter def autoDownsample(self, new_value): """ Whether we should or not apply the autoDownsample option. Parameters ---------- new_value: bool """ if new_value != self._auto_downsample: self._auto_downsample = new_value @Property(bool) def autoSetColorbarLims(self): """ Return if we should or not auto set colorbar limits. Return ------ bool """ return self._auto_colorbar_lims @autoSetColorbarLims.setter def autoSetColorbarLims(self, new_value): """ Whether we should or not auto set colorbar limits. Parameters ---------- new_value: bool """ if new_value != self._auto_colorbar_lims: self._auto_colorbar_lims = new_value @Property(int) def imageWidth(self): """ Return the width of the image. Return ------ int """ return self._image_width @imageWidth.setter def imageWidth(self, new_width): """ Set the width of the image. Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`. Parameters ---------- new_width: int """ boo = self._image_width != int(new_width) boo &= not self._xaxischannel boo &= not self._yaxischannel if boo: self._image_width = int(new_width) @Property(int) def imageHeight(self): """ Return the height of the image. Return ------ int """ return self._image_height @Property(int) def ROIOffsetX(self): """ Return the ROI offset in X axis in pixels. Return ------ int """ return self._roi_offsetx @ROIOffsetX.setter def ROIOffsetX(self, new_offset): """ Set the ROI offset in X axis in pixels. Can be overridden by :attr:`ROIOffsetXChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsetx != int(new_offset) boo &= not self._roioffsetxchannel if boo: self._roi_offsetx = int(new_offset) self.redrawROI() @Property(int) def ROIOffsetY(self): """ Return the ROI offset in Y axis in pixels. Return ------ int """ return self._roi_offsety @ROIOffsetY.setter def ROIOffsetY(self, new_offset): """ Set the ROI offset in Y axis in pixels. Can be overridden by :attr:`ROIOffsetYChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsety != int(new_offset) boo &= not self._roioffsetychannel if boo: self._roi_offsety = int(new_offset) self.redrawROI() @Property(int) def ROIWidth(self): """ Return the ROI width in pixels. Return ------ int """ return self._roi_width @ROIWidth.setter def ROIWidth(self, new_width): """ Set the ROI width in pixels. Can be overridden by :attr:`ROIWidthChannel`. Parameters ---------- new_width: int """ if new_width is None: return boo = self._roi_width != int(new_width) boo &= not self._roiwidthchannel if boo: self._roi_width = int(new_width) self.redrawROI() @Property(int) def ROIHeight(self): """ Return the ROI height in pixels. Return ------ int """ return self._roi_height @ROIHeight.setter def ROIHeight(self, new_height): """ Set the ROI height in pixels. Can be overridden by :attr:`ROIHeightChannel`. Parameters ---------- new_height: int """ if new_height is None: return boo = self._roi_height != int(new_height) boo &= not self._roiheightchannel if boo: self._roi_height = int(new_height) self.redrawROI() @Property(bool) def normalizeData(self): """ Return True if the colors are relative to data maximum and minimum. Returns ------- bool """ return self._normalize_data @normalizeData.setter @Slot(bool) def normalizeData(self, new_norm): """ Define if the colors are relative to minimum and maximum of the data. Parameters ---------- new_norm: bool """ if self._normalize_data != new_norm: self._normalize_data = new_norm @Property(ReadingOrder) def readingOrder(self): """ Return the reading order of the :attr:`imageChannel` array. Returns ------- ReadingOrder """ return self._reading_order @readingOrder.setter def readingOrder(self, order): """ Set reading order of the :attr:`imageChannel` array. Parameters ---------- order: ReadingOrder """ if self._reading_order != order: self._reading_order = order if order == self.Clike: if self._last_xaxis_data is not None: self._image_width = self._last_xaxis_data.size if self._last_yaxis_data is not None: self._image_height = self._last_yaxis_data.size elif order == self.Fortranlike: if self._last_yaxis_data is not None: self._image_width = self._last_yaxis_data.size if self._last_xaxis_data is not None: self._image_height = self._last_xaxis_data.size @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000)) # --- Events rederivations --- def keyPressEvent(self, ev): """Handle keypress events.""" return def mouseMoveEvent(self, ev): if not self._image_item.width() or not self._image_item.height(): super().mouseMoveEvent(ev) return pos = ev.pos() posaux = self._image_item.mapFromDevice(ev.pos()) if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \ posaux.y() < 0 or posaux.y() >= self._image_item.height(): super().mouseMoveEvent(ev) return pos_scene = self._view.mapSceneToView(pos) x = round(pos_scene.x()) y = round(pos_scene.y()) if self.xAxisChannel and self._last_xaxis_data is not None: maxx = len(self._last_xaxis_data) - 1 x = x if x < maxx else maxx valx = self._last_xaxis_data[x] else: valx = x if self.yAxisChannel and self._last_yaxis_data is not None: maxy = len(self._last_yaxis_data) - 1 y = y if y < maxy else maxy valy = self._last_yaxis_data[y] else: valy = y txt = self.format_tooltip.format(valx, valy) QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(), 5000) super().mouseMoveEvent(ev) # --- Channels --- @Property(str) def imageChannel(self): """ The channel address in use for the image data . Returns ------- str Channel address """ if self._imagechannel: return str(self._imagechannel.address) else: return '' @imageChannel.setter def imageChannel(self, value): """ The channel address in use for the image data . Parameters ---------- value : str Channel address """ if self._imagechannel != value: # Disconnect old channel if self._imagechannel: self._imagechannel.disconnect() # Create and connect new channel self._imagechannel = PyDMChannel( address=value, connection_slot=self.image_connection_state_changed, value_slot=self.image_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[0] = self._imagechannel self._imagechannel.connect() @Property(str) def xAxisChannel(self): """ The channel address in use for the x-axis of image. Returns ------- str Channel address """ if self._xaxischannel: return str(self._xaxischannel.address) else: return '' @xAxisChannel.setter def xAxisChannel(self, value): """ The channel address in use for the x-axis of image. Parameters ---------- value : str Channel address """ if self._xaxischannel != value: # Disconnect old channel if self._xaxischannel: self._xaxischannel.disconnect() # Create and connect new channel self._xaxischannel = PyDMChannel( address=value, connection_slot=self.connectionStateChanged, value_slot=self.xaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[1] = self._xaxischannel self._xaxischannel.connect() @Property(str) def yAxisChannel(self): """ The channel address in use for the time axis. Returns ------- str Channel address """ if self._yaxischannel: return str(self._yaxischannel.address) else: return '' @yAxisChannel.setter def yAxisChannel(self, value): """ The channel address in use for the time axis. Parameters ---------- value : str Channel address """ if self._yaxischannel != value: # Disconnect old channel if self._yaxischannel: self._yaxischannel.disconnect() # Create and connect new channel self._yaxischannel = PyDMChannel( address=value, connection_slot=self.yaxis_connection_state_changed, value_slot=self.yaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[2] = self._yaxischannel self._yaxischannel.connect() @Property(str) def ROIOffsetXChannel(self): """ Return the channel address in use for the image ROI horizontal offset. Returns ------- str Channel address """ if self._roioffsetxchannel: return str(self._roioffsetxchannel.address) else: return '' @ROIOffsetXChannel.setter def ROIOffsetXChannel(self, value): """ Return the channel address in use for the image ROI horizontal offset. Parameters ---------- value : str Channel address """ if self._roioffsetxchannel != value: # Disconnect old channel if self._roioffsetxchannel: self._roioffsetxchannel.disconnect() # Create and connect new channel self._roioffsetxchannel = PyDMChannel( address=value, connection_slot=self.roioffsetx_connection_state_changed, value_slot=self.roioffsetx_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[3] = self._roioffsetxchannel self._roioffsetxchannel.connect() @Property(str) def ROIOffsetYChannel(self): """ Return the channel address in use for the image ROI vertical offset. Returns ------- str Channel address """ if self._roioffsetychannel: return str(self._roioffsetychannel.address) else: return '' @ROIOffsetYChannel.setter def ROIOffsetYChannel(self, value): """ Return the channel address in use for the image ROI vertical offset. Parameters ---------- value : str Channel address """ if self._roioffsetychannel != value: # Disconnect old channel if self._roioffsetychannel: self._roioffsetychannel.disconnect() # Create and connect new channel self._roioffsetychannel = PyDMChannel( address=value, connection_slot=self.roioffsety_connection_state_changed, value_slot=self.roioffsety_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[4] = self._roioffsetychannel self._roioffsetychannel.connect() @Property(str) def ROIWidthChannel(self): """ Return the channel address in use for the image ROI width. Returns ------- str Channel address """ if self._roiwidthchannel: return str(self._roiwidthchannel.address) else: return '' @ROIWidthChannel.setter def ROIWidthChannel(self, value): """ Return the channel address in use for the image ROI width. Parameters ---------- value : str Channel address """ if self._roiwidthchannel != value: # Disconnect old channel if self._roiwidthchannel: self._roiwidthchannel.disconnect() # Create and connect new channel self._roiwidthchannel = PyDMChannel( address=value, connection_slot=self.roiwidth_connection_state_changed, value_slot=self.roiwidth_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[5] = self._roiwidthchannel self._roiwidthchannel.connect() @Property(str) def ROIHeightChannel(self): """ Return the channel address in use for the image ROI height. Returns ------- str Channel address """ if self._roiheightchannel: return str(self._roiheightchannel.address) else: return '' @ROIHeightChannel.setter def ROIHeightChannel(self, value): """ Return the channel address in use for the image ROI height. Parameters ---------- value : str Channel address """ if self._roiheightchannel != value: # Disconnect old channel if self._roiheightchannel: self._roiheightchannel.disconnect() # Create and connect new channel self._roiheightchannel = PyDMChannel( address=value, connection_slot=self.roiheight_connection_state_changed, value_slot=self.roiheight_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[6] = self._roiheightchannel self._roiheightchannel.connect() def channels(self): """ Return the channels being used for this Widget. Returns ------- channels : list List of PyDMChannel objects """ return self._channels def channels_for_tools(self): """Return channels for tools.""" return [self._imagechannel]
class PyDMImageView(ImageView, PyDMWidget, PyDMColorMap, ReadingOrder): """ A PyQtGraph ImageView with support for Channels and more from PyDM. If there is no :attr:`channelWidth` it is possible to define the width of the image with the :attr:`width` property. The :attr:`normalizeData` property defines if the colors of the images are relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to the minimum and maximum values of the image. Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new image is rendered in the widget. Parameters ---------- parent : QWidget The parent widget for the Label image_channel : str, optional The channel to be used by the widget for the image data. width_channel : str, optional The channel to be used by the widget to receive the image width information """ ReadingOrder = ReadingOrder Q_ENUMS(ReadingOrder) Q_ENUMS(PyDMColorMap) color_maps = cmaps def __init__(self, parent=None, image_channel=None, width_channel=None): """Initialize widget.""" ImageView.__init__(self, parent) PyDMWidget.__init__(self) self._channels = [None, None] self.thread = None self.axes = dict({'t': None, "x": 0, "y": 1, "c": None}) self._imagechannel = None self._widthchannel = None self.image_waveform = np.zeros(0) self._image_width = 0 self._normalize_data = False self._auto_downsample = True # Hide some itens of the widget. self.ui.histogram.hide() self.getImageItem().sigImageChanged.disconnect( self.ui.histogram.imageChanged) self.ui.roiBtn.hide() self.ui.menuBtn.hide() # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 # Set default reading order of numpy array data to Fortranlike. self._reading_order = ReadingOrder.Fortranlike # Make a right-click menu for changing the color map. self.cm_group = QActionGroup(self) self.cmap_for_action = {} for cm in self.color_maps: action = self.cm_group.addAction(cmap_names[cm]) action.setCheckable(True) self.cmap_for_action[action] = cm # Set the default colormap. self._colormap = PyDMColorMap.Inferno self._cm_colors = None self.colorMap = self._colormap # Setup the redraw timer. self.needs_redraw = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawImage) self._redraw_rate = 30 self.maxRedrawRate = self._redraw_rate self.newImageSignal = self.getImageItem().sigImageChanged # Set live channels if requested on initialization if image_channel: self.imageChannel = image_channel or '' if width_channel: self.widthChannel = width_channel or '' @Property(str, designable=False) def channel(self): return @channel.setter def channel(self, ch): logger.info("Use the imageChannel property with the ImageView widget.") return def widget_ctx_menu(self): """ Fetch the Widget specific context menu. It will be populated with additional tools by `assemble_tools_menu`. Returns ------- QMenu or None If the return of this method is None a new QMenu will be created by `assemble_tools_menu`. """ self.menu = ViewBoxMenu(self.getView()) cm_menu = self.menu.addMenu("Color Map") for act in self.cmap_for_action.keys(): cm_menu.addAction(act) cm_menu.triggered.connect(self._changeColorMap) return self.menu def _changeColorMap(self, action): """ Method invoked by the colormap Action Menu. Changes the current colormap used to render the image. Parameters ---------- action : QAction """ self.colorMap = self.cmap_for_action[action] @Property(float) def colorMapMin(self): """ Minimum value for the colormap. Returns ------- float """ return self.cm_min @colorMapMin.setter @Slot(float) def colorMapMin(self, new_min): """ Set the minimum value for the colormap. Parameters ---------- new_min : float """ if self.cm_min != new_min: self.cm_min = new_min if self.cm_min > self.cm_max: self.cm_max = self.cm_min @Property(float) def colorMapMax(self): """ Maximum value for the colormap. Returns ------- float """ return self.cm_max @colorMapMax.setter @Slot(float) def colorMapMax(self, new_max): """ Set the maximum value for the colormap. Parameters ---------- new_max : float """ if self.cm_max != new_max: self.cm_max = new_max if self.cm_max < self.cm_min: self.cm_min = self.cm_max def setColorMapLimits(self, mn, mx): """ Set the limit values for the colormap. Parameters ---------- mn : int The lower limit mx : int The upper limit """ if mn >= mx: return self.cm_max = mx self.cm_min = mn @Property(PyDMColorMap) def colorMap(self): """ Return the color map used by the ImageView. Returns ------- PyDMColorMap """ return self._colormap @colorMap.setter def colorMap(self, new_cmap): """ Set the color map used by the ImageView. Parameters ------- new_cmap : PyDMColorMap """ self._colormap = new_cmap self._cm_colors = self.color_maps[new_cmap] self.setColorMap() for action in self.cm_group.actions(): if self.cmap_for_action[action] == self._colormap: action.setChecked(True) else: action.setChecked(False) def setColorMap(self, cmap=None): """ Update the image colormap. Parameters ---------- cmap : ColorMap """ if not cmap: if not self._cm_colors.any(): return # Take default values pos = np.linspace(0.0, 1.0, num=len(self._cm_colors)) cmap = ColorMap(pos, self._cm_colors) self.getView().setBackgroundColor(cmap.map(0)) lut = cmap.getLookupTable(0.0, 1.0, alpha=False) self.getImageItem().setLookupTable(lut) @Slot(bool) def image_connection_state_changed(self, conn): """ Callback invoked when the Image Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ if conn: self.redraw_timer.start() else: self.redraw_timer.stop() @Slot(np.ndarray) def image_value_changed(self, new_image): """ Callback invoked when the Image Channel value is changed. We try to do as little as possible in this method, because it gets called every time the image channel updates, which might be extremely often. Basically just store the data, and set a flag requesting that the image be redrawn. Parameters ---------- new_image : np.ndarray The new image data. This can be a flat 1D array, or a 2D array. """ if new_image is None or new_image.size == 0: return logging.debug("ImageView Received New Image - Needs Redraw -> True") self.image_waveform = new_image self.needs_redraw = True @Slot(int) def image_width_changed(self, new_width): """ Callback invoked when the Image Width Channel value is changed. Parameters ---------- new_width : int The new image width """ if new_width is None: return self._image_width = int(new_width) def process_image(self, image): """ Boilerplate method to be used by applications in order to add calculations and also modify the image before it is displayed at the widget. .. warning:: This code runs in a separated QThread so it **MUST** not try to write to QWidgets. Parameters ---------- image : np.ndarray The Image Data as a 2D numpy array Returns ------- np.ndarray The Image Data as a 2D numpy array after processing. """ return image def redrawImage(self): """ Set the image data into the ImageItem, if needed. If necessary, reshape the image to 2D first. """ if self.thread is not None and not self.thread.isFinished(): logger.warning( "Image processing has taken longer than the refresh rate.") return self.thread = ImageUpdateThread(self) self.thread.updateSignal.connect(self.__updateDisplay) logging.debug("ImageView RedrawImage Thread Launched") self.thread.start() @Slot(list) def __updateDisplay(self, data): logging.debug("ImageView Update Display with new image") mini, maxi = data[0], data[1] img = data[2] self.getImageItem().setLevels([mini, maxi]) self.getImageItem().setImage( img, autoLevels=False, autoDownsample=self.autoDownsample) @Property(bool) def autoDownsample(self): """ Return if we should or not apply the autoDownsample option to PyQtGraph. Return ------ bool """ return self._auto_downsample @autoDownsample.setter def autoDownsample(self, new_value): """ Whether we should or not apply the autoDownsample option to PyQtGraph. Parameters ---------- new_value: bool """ if new_value != self._auto_downsample: self._auto_downsample = new_value @Property(int) def imageWidth(self): """ Return the width of the image. Return ------ int """ return self._image_width @imageWidth.setter def imageWidth(self, new_width): """ Set the width of the image. Can be overridden by :attr:`widthChannel`. Parameters ---------- new_width: int """ if (self._image_width != int(new_width) and (self._widthchannel is None or self._widthchannel == '')): self._image_width = int(new_width) @Property(bool) def normalizeData(self): """ Return True if the colors are relative to data maximum and minimum. Returns ------- bool """ return self._normalize_data @normalizeData.setter @Slot(bool) def normalizeData(self, new_norm): """ Define if the colors are relative to minimum and maximum of the data. Parameters ---------- new_norm: bool """ if self._normalize_data != new_norm: self._normalize_data = new_norm @Property(ReadingOrder) def readingOrder(self): """ Return the reading order of the :attr:`imageChannel` array. Returns ------- ReadingOrder """ return self._reading_order @readingOrder.setter def readingOrder(self, new_order): """ Set reading order of the :attr:`imageChannel` array. Parameters ---------- new_order: ReadingOrder """ if self._reading_order != new_order: self._reading_order = new_order def keyPressEvent(self, ev): """Handle keypress events.""" return @Property(str) def imageChannel(self): """ The channel address in use for the image data . Returns ------- str Channel address """ if self._imagechannel: return str(self._imagechannel.address) else: return '' @imageChannel.setter def imageChannel(self, value): """ The channel address in use for the image data . Parameters ---------- value : str Channel address """ if self._imagechannel != value: # Disconnect old channel if self._imagechannel: self._imagechannel.disconnect() # Create and connect new channel self._imagechannel = PyDMChannel( address=value, connection_slot=self.image_connection_state_changed, value_slot=self.image_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[0] = self._imagechannel self._imagechannel.connect() @Property(str) def widthChannel(self): """ The channel address in use for the image width . Returns ------- str Channel address """ if self._widthchannel: return str(self._widthchannel.address) else: return '' @widthChannel.setter def widthChannel(self, value): """ The channel address in use for the image width . Parameters ---------- value : str Channel address """ if self._widthchannel != value: # Disconnect old channel if self._widthchannel: self._widthchannel.disconnect() # Create and connect new channel self._widthchannel = PyDMChannel( address=value, connection_slot=self.connectionStateChanged, value_slot=self.image_width_changed, severity_slot=self.alarmSeverityChanged) self._channels[1] = self._widthchannel self._widthchannel.connect() def channels(self): """ Return the channels being used for this Widget. Returns ------- channels : list List of PyDMChannel objects """ return self._channels def channels_for_tools(self): """Return channels for tools.""" return [self._imagechannel] @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))