Source code for datalab.gui.actionhandler

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Action handler
==============

The :mod:`datalab.gui.actionhandler` module handles all application actions
(menus, toolbars, context menu). These actions point to DataLab panels, processors,
objecthandler, ...

Utility classes
---------------

.. autoclass:: SelectCond
    :members:

.. autoclass:: ActionCategory
    :members:

Handler classes
---------------

.. autoclass:: SignalActionHandler
    :members:
    :inherited-members:

.. autoclass:: ImageActionHandler
    :members:
    :inherited-members:
"""

# pylint: disable=invalid-name  # Allows short reference names like x, y, ...

from __future__ import annotations

import abc
import enum
from collections.abc import Callable, Generator
from contextlib import contextmanager
from functools import partial
from typing import TYPE_CHECKING

import sigima.objects as sio
from guidata.configtools import get_icon
from guidata.qthelpers import add_actions, create_action
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW

from datalab.adapters_metadata import GeometryAdapter, TableAdapter, have_results
from datalab.config import Conf, _
from datalab.gui import newobject
from datalab.widgets import fitdialog

if TYPE_CHECKING:
    from sigima.objects import ImageObj, SignalObj

    from datalab.gui.panel.image import ImagePanel
    from datalab.gui.panel.signal import SignalPanel
    from datalab.objectmodel import ObjectGroup


[docs] class SelectCond: """Signal or image select conditions""" @staticmethod def __compat_groups(selected_groups: list[ObjectGroup], min_len: int = 1) -> bool: """Check if groups are compatible""" return ( len(selected_groups) >= min_len and all(len(group) == len(selected_groups[0]) for group in selected_groups) and all(len(group) > 0 for group in selected_groups) )
[docs] @staticmethod # pylint: disable=unused-argument def always( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Always true""" return True
[docs] @staticmethod def exactly_one( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Exactly one signal or image is selected""" return len(selected_groups) == 0 and len(selected_objects) == 1
[docs] @staticmethod # pylint: disable=unused-argument def exactly_one_group( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Exactly one group is selected""" return len(selected_groups) == 1
[docs] @staticmethod def exactly_one_group_or_one_object( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Exactly one group or one signal or image is selected""" return len(selected_groups) == 1 or len(selected_objects) == 1
[docs] @staticmethod # pylint: disable=unused-argument def at_least_one_group_or_one_object( sel_groups: list[ObjectGroup], sel_objects: list[SignalObj | ImageObj], ) -> bool: """At least one group or one signal or image is selected""" return len(sel_objects) >= 1 or len(sel_groups) >= 1
[docs] @staticmethod # pylint: disable=unused-argument def at_least_one( sel_groups: list[ObjectGroup], sel_objects: list[SignalObj | ImageObj], ) -> bool: """At least one signal or image is selected""" return len(sel_objects) >= 1 or SelectCond.__compat_groups(sel_groups, 1)
[docs] @staticmethod def at_least_two( sel_groups: list[ObjectGroup], sel_objects: list[SignalObj | ImageObj], ) -> bool: """At least two signals or images are selected""" return len(sel_objects) >= 2 or SelectCond.__compat_groups(sel_groups, 2)
[docs] @staticmethod # pylint: disable=unused-argument def with_roi( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """At least one signal or image has a ROI""" return any(obj.roi is not None for obj in selected_objects)
[docs] @staticmethod # pylint: disable=unused-argument def exactly_one_with_roi( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Exactly one signal or image has a ROI""" return ( len(selected_groups) == 0 and len(selected_objects) == 1 and selected_objects[0].roi is not None )
[docs] @staticmethod # pylint: disable=unused-argument def exactly_one_with_annotations( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Exactly one signal or image has annotations""" return ( len(selected_groups) == 0 and len(selected_objects) == 1 and selected_objects[0].has_annotations() )
[docs] @staticmethod # pylint: disable=unused-argument def with_annotations( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """At least one signal or image has annotations""" return any(obj.has_annotations() for obj in selected_objects)
[docs] @staticmethod # pylint: disable=unused-argument def with_results( selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """At least one signal or image has results""" return have_results(selected_objects)
[docs] class ActionCategory(enum.Enum): """Action categories""" FILE = enum.auto() CREATE = enum.auto() EDIT = enum.auto() VIEW = enum.auto() ROI = enum.auto() OPERATION = enum.auto() PROCESSING = enum.auto() ANALYSIS = enum.auto() CONTEXT_MENU = enum.auto() PANEL_TOOLBAR = enum.auto() VIEW_TOOLBAR = enum.auto() SUBMENU = enum.auto() # temporary PLUGINS = enum.auto() # for plugins actions
class BaseActionHandler(metaclass=abc.ABCMeta): """Object handling panel GUI interactions: actions, menus, ... Args: panel: Panel to handle panel_toolbar: Panel toolbar (actions related to the panel objects management) view_toolbar: View toolbar (actions related to the panel view, i.e. plot) """ OBJECT_STR = "" # e.g. "signal" OBJECT_STR_PLURAL = "" # e.g. "signals" def __init__( self, panel: SignalPanel | ImagePanel, panel_toolbar: QW.QToolBar, view_toolbar: QW.QToolBar, ): self.panel = panel self.panel_toolbar = panel_toolbar self.view_toolbar = view_toolbar self.feature_actions = {} self.operation_end_actions = None self.__category_in_progress: ActionCategory = None self.__submenu_in_progress = False self.__submenu_stack: list[dict[str, any]] = [] # Stack for nested submenus self.__actions: dict[Callable, list[QW.QAction]] = {} self.__submenus: dict[str, QW.QMenu] = {} # Store reference to ROI remove submenu self.roi_remove_submenu: QW.QMenu | None = None # Store reference to results delete submenu self.results_delete_submenu: QW.QMenu | None = None # Store reference to metadata and annotations submenus (for screenshots) self.metadata_submenu: QW.QMenu | None = None self.annotations_submenu: QW.QMenu | None = None # Store reference to show label action (for settings dialog) self.show_label_action: QW.QAction | None = None @property def object_suffix(self) -> str: """Object suffix (e.g. "sig" for signal, "ima" for image)""" return self.__class__.__name__[:3].lower() def populate_roi_remove_submenu(self) -> None: """Populate the ROI Remove submenu dynamically based on current selection""" submenu = self.roi_remove_submenu if submenu is None: return # Clear existing actions submenu.clear() # Get current selected object selected_objects = self.panel.objview.get_sel_objects() if not selected_objects: return obj = selected_objects[0] if obj.roi is None or obj.roi.is_empty(): return # Add individual ROI removal actions for i in range(len(obj.roi.single_rois)): roi_title = obj.roi.get_single_roi_title(i) action = QW.QAction(roi_title, submenu) # Use partial to avoid lambda closure issues action.triggered.connect(partial(self._remove_single_roi_by_index, i)) submenu.addAction(action) # Add separator and "Remove all" action if len(obj.roi.single_rois) > 0: submenu.addSeparator() remove_all_action = QW.QAction(_("Remove all"), submenu) remove_all_action.triggered.connect( self.panel.processor.delete_regions_of_interest ) submenu.addAction(remove_all_action) def _remove_single_roi_by_index(self, roi_index: int) -> None: """Helper method to remove a single ROI by index""" self.panel.processor.delete_single_roi(roi_index) def has_metadata_in_clipboard( self, selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Check if metadata clipboard is not empty""" # pylint: disable=unused-argument return bool(self.panel.metadata_clipboard) def has_annotations_in_clipboard( self, selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> bool: """Check if annotations clipboard is not empty""" # pylint: disable=unused-argument return bool(self.panel.annotations_clipboard) def populate_results_delete_submenu(self) -> None: """Populate the Results Delete submenu dynamically based on current selection""" submenu = self.results_delete_submenu if submenu is None: return # Clear existing actions submenu.clear() # Get current selected objects (including groups) objs = self.panel.objview.get_sel_objects(include_groups=True) if not objs: submenu.setEnabled(False) return # Collect all results with their metadata keys and titles result_items = [] # List of (metadata_key, title, obj, adapter) for obj in objs: # Get all adapters for this object for adapter in list(GeometryAdapter.iterate_from_obj(obj)) + list( TableAdapter.iterate_from_obj(obj) ): metadata_key = adapter.metadata_key title = adapter.title result_items.append((metadata_key, title, obj, adapter)) if not result_items: submenu.setEnabled(False) return # Enable submenu since we have results submenu.setEnabled(True) # Add individual result deletion actions for metadata_key, title, obj, adapter in result_items: action = QW.QAction(title, submenu) # Use partial to avoid lambda closure issues action.triggered.connect(partial(self._delete_single_result, obj, adapter)) submenu.addAction(action) # Add separator and "Delete all results..." action if result_items: submenu.addSeparator() delete_all_action = QW.QAction(_("Delete all results") + "...", submenu) delete_all_action.triggered.connect(self.panel.delete_results) submenu.addAction(delete_all_action) def _delete_single_result( self, obj: SignalObj | ImageObj, adapter: GeometryAdapter | TableAdapter ) -> None: """Helper method to delete a single result Args: obj: Object containing the result adapter: Adapter for the result to delete """ adapter.remove_from(obj) # Update properties panel to reflect the removal if obj is self.panel.objview.get_current_object(): self.panel.objprop.update_properties_from(obj) # Update action states to reflect the removal selected_groups = self.panel.objview.get_sel_groups() selected_objects = self.panel.objview.get_sel_objects(include_groups=True) self.selected_objects_changed(selected_groups, selected_objects) # Refresh the plot to update the display # Use the same refresh pattern as delete_results() method self.panel.refresh_plot("selected", True, False) @contextmanager def new_category(self, category: ActionCategory) -> Generator[None, None, None]: """Context manager for creating a new menu. Args: category: Action category Yields: None """ self.__category_in_progress = category try: yield finally: self.__category_in_progress = None @contextmanager def new_menu( self, title: str, icon_name: str | None = None, store_ref: str | None = None, ) -> Generator[None, None, None]: """Context manager for creating a new menu. Args: title: Menu title icon_name: Menu icon name. Defaults to None. store_ref: Optional attribute name to store menu reference. Defaults to None. Yields: None """ # Create a unique key for this submenu level parent_key = "" if self.__submenu_stack: parent_key = self.__submenu_stack[-1]["key"] + "/" elif self.__category_in_progress: parent_key = self.__category_in_progress.name + "/" key = parent_key + title is_new = key not in self.__submenus if is_new: self.__submenus[key] = menu = QW.QMenu(title) if icon_name: menu.setIcon(get_icon(icon_name)) # Store reference to menu if requested if store_ref is not None: setattr(self, store_ref, menu) else: menu = self.__submenus[key] # Save current submenu state and push new submenu onto stack submenu_state = { "key": key, "menu": menu, "is_new": is_new, "actions": [], # Actions for this submenu level } self.__submenu_stack.append(submenu_state) self.__submenu_in_progress = True try: yield finally: # Pop the current submenu from stack current_submenu = self.__submenu_stack.pop() # Get actions for this specific submenu level submenu_actions = current_submenu.get("actions", []) # Also get any actions that were added to the generic SUBMENU category # while this submenu was the active one generic_submenu_actions = self.feature_actions.pop( ActionCategory.SUBMENU, [] ) submenu_actions.extend(generic_submenu_actions) add_actions(current_submenu["menu"], submenu_actions) # Update submenu in progress status BEFORE adding to parent self.__submenu_in_progress = len(self.__submenu_stack) > 0 if current_submenu["is_new"]: # Add this submenu to its parent (either category or parent submenu) if self.__submenu_stack: # We're in a nested submenu, add to parent submenu's actions parent_submenu = self.__submenu_stack[-1] parent_submenu["actions"].append(current_submenu["menu"]) else: # We're at the top level, add to category actions # Force using the current category, not SUBMENU self.add_to_action_list( current_submenu["menu"], category=self.__category_in_progress ) # pylint: disable=too-many-positional-arguments def new_action( self, title: str, position: int | None = None, separator: bool = False, triggered: Callable | None = None, toggled: Callable | None = None, shortcut: QW.QShortcut | None = None, icon_name: str | None = None, tip: str | None = None, select_condition: Callable | str | None = None, context_menu_pos: int | None = None, context_menu_sep: bool = False, toolbar_pos: int | None = None, toolbar_sep: bool = False, toolbar_category: ActionCategory | None = None, ) -> QW.QAction: """Create new action and add it to list of actions. Args: title: action title position: add action to menu at this position. Defaults to None. separator: add separator before action in menu (or after if pos is positive). Defaults to False. triggered: triggered callback. Defaults to None. toggled: toggled callback. Defaults to None. shortcut: shortcut. Defaults to None. icon_name: icon name. Defaults to None. tip: tooltip. Defaults to None. select_condition: selection condition. Defaults to None. If str, must be the name of a method of SelectCond, i.e. one of "always", "exactly_one", "exactly_one_group", "at_least_one_group_or_one_object", "at_least_one", "at_least_two", "with_roi". context_menu_pos: add action to context menu at this position. Defaults to None. context_menu_sep: add separator before action in context menu (or after if context_menu_pos is positive). Defaults to False. toolbar_pos: add action to toolbar at this position. Defaults to None. toolbar_sep: add separator before action in toolbar (or after if toolbar_pos is positive). Defaults to False. toolbar_category: toolbar category. Defaults to None. If toolbar_pos is not None, this specifies the category of the toolbar. If None, defaults to ActionCategory.VIEW_TOOLBAR if the current category is ActionCategory.VIEW, else to ActionCategory.PANEL_TOOLBAR. Returns: New action """ if isinstance(select_condition, str): assert select_condition in SelectCond.__dict__ select_condition = getattr(SelectCond, select_condition) action = create_action( parent=self.panel, title=title, triggered=triggered, toggled=toggled, shortcut=shortcut, icon=get_icon(icon_name) if icon_name else None, tip=tip, context=QC.Qt.WidgetWithChildrenShortcut, # [1] ) self.panel.addAction(action) # [1] # [1] This is needed to make actions work with shortcuts for active panel, # because some of the shortcuts are using the same keybindings for both panels. # (Fixes #10) self.add_action(action, select_condition) self.add_to_action_list(action, None, position, separator) if context_menu_pos is not None: self.add_to_action_list( action, ActionCategory.CONTEXT_MENU, context_menu_pos, context_menu_sep ) if toolbar_pos is not None: if toolbar_category is None: if self.__category_in_progress is ActionCategory.VIEW: toolbar_category = ActionCategory.VIEW_TOOLBAR else: toolbar_category = ActionCategory.PANEL_TOOLBAR self.add_to_action_list(action, toolbar_category, toolbar_pos, toolbar_sep) return action def action_for( self, function_or_name: Callable | str, position: int | None = None, separator: bool = False, context_menu_pos: int | None = None, context_menu_sep: bool = False, toolbar_pos: int | None = None, toolbar_sep: bool = False, toolbar_category: ActionCategory | None = None, ) -> QW.QAction: """Create action for a feature. Args: function_or_name: function or name of the feature position: add action to menu at this position. Defaults to None. separator: add separator before action in menu context_menu_pos: add action to context menu at this position. context_menu_pos: add action to context menu at this position. Defaults to None. context_menu_sep: add separator before action in context menu (or after if context_menu_pos is positive). Defaults to False. toolbar_pos: add action to toolbar at this position. Defaults to None. toolbar_sep: add separator before action in toolbar (or after if toolbar_pos is positive). Defaults to False. toolbar_category: toolbar category. Defaults to None. If toolbar_pos is not None, this specifies the category of the toolbar. If None, defaults to ActionCategory.VIEW_TOOLBAR if the current category is ActionCategory.VIEW, else to ActionCategory.PANEL_TOOLBAR. Returns: New action """ feature = self.panel.processor.get_feature(function_or_name) if feature.pattern == "n_to_1": condition = SelectCond.at_least_two else: condition = SelectCond.at_least_one return self.new_action( feature.action_title, position=position, separator=separator, triggered=lambda: self.panel.processor.run_feature(feature.function), select_condition=condition, icon_name=feature.icon_name, tip=feature.comment, context_menu_pos=context_menu_pos, context_menu_sep=context_menu_sep, toolbar_pos=toolbar_pos, toolbar_sep=toolbar_sep, toolbar_category=toolbar_category, ) def add_to_action_list( self, action: QW.QAction, category: ActionCategory | None = None, pos: int | None = None, sep: bool = False, ) -> None: """Add action to list of actions. Args: action: action to add category: action category. Defaults to None. If None, action is added to the current category. pos: add action to menu at this position. Defaults to None. If None, action is added at the end of the list. sep: add separator before action in menu (or after if pos is positive). Defaults to False. """ if category is None: if self.__submenu_in_progress and self.__submenu_stack: # Add directly to the current submenu's action list current_submenu = self.__submenu_stack[-1] actionlist = current_submenu["actions"] if pos is None: pos = -1 add_separator_after = pos >= 0 if pos < 0: pos = len(actionlist) + pos + 1 actionlist.insert(pos, action) if sep: if add_separator_after: pos += 1 actionlist.insert(pos, None) return if self.__category_in_progress is not None: category = self.__category_in_progress else: raise ValueError("No category specified") if pos is None: pos = -1 actionlist = self.feature_actions.setdefault(category, []) add_separator_after = pos >= 0 if pos < 0: pos = len(actionlist) + pos + 1 actionlist.insert(pos, action) if sep: if add_separator_after: pos += 1 actionlist.insert(pos, None) def add_action( self, action: QW.QAction, select_condition: Callable | None = None ) -> None: """Add action to list of actions. Args: action: action to add select_condition: condition to enable action. Defaults to None. If None, action is enabled if at least one object is selected. """ if select_condition is None: select_condition = SelectCond.at_least_one self.__actions.setdefault(select_condition, []).append(action) def selected_objects_changed( self, selected_groups: list[ObjectGroup], selected_objects: list[SignalObj | ImageObj], ) -> None: """Update actions based on selected objects. Args: selected_groups: selected groups selected_objects: selected objects """ for cond, actlist in self.__actions.items(): if cond is not None: for act in actlist: act.setEnabled(cond(selected_groups, selected_objects)) def create_all_actions(self): """Create all actions""" self.create_first_actions() self.create_last_actions() # Connect ROI remove submenu signal after all actions are created if self.roi_remove_submenu is not None: self.roi_remove_submenu.aboutToShow.connect( self.populate_roi_remove_submenu ) # Add the submenu to the action management system with ROI condition self.add_action(self.roi_remove_submenu, SelectCond.with_roi) # Connect results delete submenu signal after all actions are created if self.results_delete_submenu is not None: self.results_delete_submenu.aboutToShow.connect( self.populate_results_delete_submenu ) # Add the submenu to the action management system self.add_action( self.results_delete_submenu, SelectCond.at_least_one_group_or_one_object, ) add_actions( self.panel_toolbar, self.feature_actions.pop(ActionCategory.PANEL_TOOLBAR) ) # For the view toolbar, we add the actions to the beginning of the toolbar: before = self.view_toolbar.actions()[0] for action in self.feature_actions.pop(ActionCategory.VIEW_TOOLBAR): if action is None: self.view_toolbar.insertSeparator(before) else: self.view_toolbar.insertAction(before, action) self.view_toolbar.insertSeparator(before) def create_first_actions(self): """Create actions that are added to the menus in the first place""" # MARK: FILE with self.new_category(ActionCategory.FILE): self.new_action( _("Open %s...") % self.OBJECT_STR, # Icon name is 'fileopen_sig.svg' or 'fileopen_ima.svg': icon_name=f"fileopen_{self.object_suffix}.svg", tip=_("Open one or more %s files") % self.OBJECT_STR, triggered=self.panel.load_from_files, shortcut=QG.QKeySequence(QG.QKeySequence.Open), select_condition=SelectCond.always, toolbar_pos=-1, ) self.new_action( _("Open from directory..."), icon_name="fileopen_directory.svg", tip=_("Open all %s files from directory") % self.OBJECT_STR, triggered=self.panel.load_from_directory, select_condition=SelectCond.always, toolbar_pos=-1, ) self.new_action( _("Save %s...") % self.OBJECT_STR, # Icon name is 'filesave_sig.svg' or 'filesave_ima.svg' icon_name=f"filesave_{self.object_suffix}.svg", tip=_("Save selected %s") % self.OBJECT_STR_PLURAL, triggered=self.panel.save_to_files, shortcut=QG.QKeySequence(QG.QKeySequence.Save), select_condition=SelectCond.at_least_one, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Save to directory..."), icon_name="save_to_directory.svg", tip=_("Save selected %s using a filename pattern") % self.OBJECT_STR_PLURAL, triggered=self.panel.save_to_directory, select_condition=SelectCond.at_least_two, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Import text file..."), icon_name="import_text.svg", triggered=self.panel.exec_import_wizard, select_condition=SelectCond.always, ) # MARK: EDIT with self.new_category(ActionCategory.EDIT): self.new_action( _("Recompute"), icon_name="recompute.svg", shortcut="Ctrl+R", tip=_("Recompute selected %s with its processing parameters") % self.OBJECT_STR, triggered=self.panel.recompute_processing, select_condition=SelectCond.at_least_one_group_or_one_object, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Select source objects"), icon_name="goto_source.svg", tip=_("Select source object(s) used to create the selected %s") % self.OBJECT_STR, triggered=self.panel.select_source_objects, select_condition=SelectCond.exactly_one, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Rename"), icon_name="rename.svg", shortcut="F2", tip=_("Edit title of selected %s or group") % self.OBJECT_STR, triggered=self.panel.rename_selected_object_or_group, select_condition=SelectCond.exactly_one_group_or_one_object, context_menu_pos=-1, separator=True, ) self.new_action( _("New group..."), icon_name="new_group.svg", tip=_("Create a new group"), triggered=self.panel.new_group, select_condition=SelectCond.always, context_menu_pos=-1, ) self.new_action( _("Move up"), icon_name="move_up.svg", tip=_("Move up selection (groups or objects)"), triggered=self.panel.objview.move_up, select_condition=SelectCond.at_least_one_group_or_one_object, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Move down"), icon_name="move_down.svg", tip=_("Move down selection (groups or objects)"), triggered=self.panel.objview.move_down, select_condition=SelectCond.at_least_one_group_or_one_object, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Duplicate"), icon_name="duplicate.svg", tip=_("Duplicate selected %s") % self.OBJECT_STR, separator=True, triggered=self.panel.duplicate_object, shortcut=QG.QKeySequence(QG.QKeySequence.Copy), select_condition=SelectCond.at_least_one_group_or_one_object, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Remove"), icon_name="delete.svg", tip=_("Remove selected %s") % self.OBJECT_STR, triggered=self.panel.remove_object, shortcut=QG.QKeySequence(QG.QKeySequence.Delete), select_condition=SelectCond.at_least_one_group_or_one_object, context_menu_pos=-1, toolbar_pos=-1, ) self.new_action( _("Delete all"), select_condition=SelectCond.always, shortcut="Shift+Ctrl+Suppr", tip=_("Delete all groups and objects"), icon_name="delete_all.svg", triggered=self.panel.delete_all_objects, toolbar_pos=-1, ) with self.new_menu( _("Metadata"), icon_name="metadata.svg", store_ref="metadata_submenu" ): self.new_action( _("Copy metadata"), icon_name="metadata_copy.svg", tip=_("Copy metadata from selected %s") % self.OBJECT_STR, triggered=self.panel.copy_metadata, select_condition=SelectCond.exactly_one, toolbar_pos=-1, ) self.new_action( _("Paste metadata"), icon_name="metadata_paste.svg", tip=_("Paste metadata into selected %s") % self.OBJECT_STR, triggered=self.panel.paste_metadata, select_condition=self.has_metadata_in_clipboard, toolbar_pos=-1, ) self.new_action( _("Add metadata") + "...", separator=True, icon_name="metadata_add.svg", tip=_("Add a metadata item to selected %s") % self.OBJECT_STR, triggered=self.panel.add_metadata, select_condition=SelectCond.at_least_one, toolbar_pos=-1, ) self.new_action( _("Import metadata") + "...", icon_name="metadata_import.svg", tip=_("Import metadata into %s") % self.OBJECT_STR, triggered=self.panel.import_metadata_from_file, select_condition=SelectCond.exactly_one, toolbar_pos=-1, ) self.new_action( _("Export metadata") + "...", icon_name="metadata_export.svg", tip=_("Export selected %s metadata") % self.OBJECT_STR, triggered=self.panel.export_metadata_from_file, select_condition=SelectCond.exactly_one, toolbar_pos=-1, ) self.new_action( _("Delete object metadata"), separator=True, icon_name="metadata_delete.svg", tip=_("Delete all that is contained in object metadata"), triggered=self.panel.delete_metadata, toolbar_pos=-1, ) with self.new_menu( _("Annotations"), icon_name="annotations.svg", store_ref="annotations_submenu", ): self.new_action( _("Copy annotations"), icon_name="annotations_copy.svg", tip=_("Copy annotations from selected %s") % self.OBJECT_STR, triggered=self.panel.copy_annotations, select_condition=SelectCond.exactly_one_with_annotations, ) self.new_action( _("Paste annotations"), icon_name="annotations_paste.svg", tip=_("Paste annotations into selected %s") % self.OBJECT_STR, triggered=self.panel.paste_annotations, select_condition=self.has_annotations_in_clipboard, ) self.new_action( _("Edit annotations") + "...", separator=True, icon_name="annotations_edit.svg", tip=_("Edit annotations of selected %s") % self.OBJECT_STR, triggered=lambda: self.panel.open_separate_view( edit_annotations=True ), select_condition=SelectCond.exactly_one, ) self.new_action( _("Import annotations") + "...", icon_name="annotations_import.svg", tip=_("Import annotations into %s") % self.OBJECT_STR, triggered=self.panel.import_annotations_from_file, select_condition=SelectCond.exactly_one, ) self.new_action( _("Export annotations") + "...", icon_name="annotations_export.svg", tip=_("Export selected %s annotations") % self.OBJECT_STR, triggered=self.panel.export_annotations_from_file, select_condition=SelectCond.exactly_one_with_annotations, ) self.new_action( _("Delete annotations"), separator=True, icon_name="annotations_delete.svg", tip=_("Delete all annotations from selected %s") % self.OBJECT_STR, triggered=self.panel.delete_annotations, select_condition=SelectCond.with_annotations, ) self.new_action( _("Insert object title as annotation label"), separator=True, triggered=lambda: self.panel.add_label_with_title(ignore_msg=False), tip=_( "Add the selected object's title as a label to the plot annotations" ), ) self.new_action( _("Copy titles to clipboard"), icon_name="copy_titles.svg", tip=_("Copy titles of selected objects to clipboard"), triggered=self.panel.copy_titles_to_clipboard, ) # MARK: ROI with self.new_category(ActionCategory.ROI): self.new_action( _("Edit graphically") + "...", triggered=self.panel.processor.edit_roi_graphically, icon_name="roi.svg", context_menu_pos=-1, context_menu_sep=True, toolbar_pos=-1, toolbar_category=ActionCategory.VIEW_TOOLBAR, tip=_("Edit regions of interest graphically"), ) self.new_action( _("Edit numerically") + "...", triggered=self.panel.processor.edit_roi_numerically, select_condition=SelectCond.exactly_one_with_roi, tip=_("Edit regions of interest numerically"), ) # MARK: VIEW with self.new_category(ActionCategory.VIEW): self.new_action( _("View in a new window") + "...", icon_name="new_window.svg", tip=_("View selected %s in a new window") % self.OBJECT_STR, triggered=self.panel.open_separate_view, context_menu_pos=0, context_menu_sep=True, toolbar_pos=0, ) # MARK: OPERATION with self.new_category(ActionCategory.OPERATION): self.action_for("arithmetic") with self.new_menu(_("Constant Operations"), icon_name="constant.svg"): self.action_for("addition_constant") self.action_for("difference_constant") self.action_for("product_constant") self.action_for("division_constant") self.action_for("addition") self.action_for("difference") self.action_for("product") self.action_for("division") self.action_for("inverse", separator=True) self.action_for("exp") self.action_for("log10") # MARK: PROCESSING with self.new_category(ActionCategory.PROCESSING): with self.new_menu( _("Axis transformation"), icon_name="axis_transform.svg" ): self.action_for("calibration") with self.new_menu(_("Level adjustment"), icon_name="level_adjustment.svg"): self.action_for("normalize") self.action_for("clip") self.new_action( _("Offset correction"), triggered=self.panel.processor.compute_offset_correction, icon_name="offset_correction.svg", tip=_("Evaluate and subtract the offset value from the data"), ) with self.new_menu(_("Noise addition"), icon_name="noise_addition.svg"): self.action_for("add_gaussian_noise") self.action_for("add_poisson_noise") self.action_for("add_uniform_noise") with self.new_menu(_("Noise reduction"), icon_name="noise_reduction.svg"): self.action_for("gaussian_filter") self.action_for("moving_average") self.action_for("moving_median") self.action_for("wiener") with self.new_menu(_("Fourier analysis"), icon_name="fourier.svg"): self.action_for("zero_padding") self.action_for("fft") self.action_for("ifft") self.action_for("magnitude_spectrum") self.action_for("phase_spectrum") self.action_for("psd") # MARK: ANALYSIS with self.new_category(ActionCategory.ANALYSIS): self.action_for("stats", context_menu_pos=-1, context_menu_sep=True) self.action_for("histogram", context_menu_pos=-1) def create_last_actions(self): """Create actions that are added to the menus in the end""" # MARK: ROI with self.new_category(ActionCategory.ROI): self.new_action( _("Extract") + "...", triggered=self.panel.processor.compute_roi_extraction, # Icon name is 'roi_sig.svg' or 'roi_ima.svg': icon_name=f"roi_{self.object_suffix}.svg", separator=True, ) self.new_action( _("Copy"), separator=True, icon_name="roi_copy.svg", tip=_("Copy regions of interest from selected %s") % self.OBJECT_STR, triggered=self.panel.copy_roi, select_condition=SelectCond.exactly_one_with_roi, toolbar_pos=-1, ) self.new_action( _("Paste"), icon_name="roi_paste.svg", tip=_("Paste regions of interest into selected %s") % self.OBJECT_STR, triggered=self.panel.paste_roi, toolbar_pos=-1, ) self.new_action( _("Import") + "...", icon_name="roi_import.svg", tip=_("Import regions of interest into %s") % self.OBJECT_STR, triggered=self.panel.import_roi_from_file, select_condition=SelectCond.exactly_one, toolbar_pos=-1, ) self.new_action( _("Export") + "...", icon_name="roi_export.svg", tip=_("Export selected %s regions of interest") % self.OBJECT_STR, triggered=self.panel.export_roi_to_file, select_condition=SelectCond.exactly_one_with_roi, toolbar_pos=-1, ) # Create dynamic "Remove" submenu with self.new_menu(_("Remove"), icon_name="roi_delete.svg"): # Store reference to the submenu for dynamic population if self.__submenu_stack: current_submenu = self.__submenu_stack[-1] self.roi_remove_submenu = current_submenu["menu"] # MARK: OPERATION with self.new_category(ActionCategory.OPERATION): self.action_for("absolute", separator=True) self.action_for("phase") self.action_for("complex_from_magnitude_phase") self.action_for("real", separator=True) self.action_for("imag") self.action_for("complex_from_real_imag") self.action_for("astype", separator=True) self.action_for("average", separator=True) self.action_for("standard_deviation") self.action_for("quadratic_difference") self.action_for("convolution", separator=True) self.action_for("deconvolution") # MARK: ANALYSIS with self.new_category(ActionCategory.ANALYSIS): self.new_action( _("Show results") + "...", triggered=self.panel.show_results, icon_name="show_results.svg", separator=True, select_condition=SelectCond.at_least_one_group_or_one_object, ) self.show_label_action = self.new_action( _("Results label"), toggled=self.panel.toggle_result_label_visibility, tip=_("Show or hide the merged result label on the plot"), select_condition=SelectCond.with_results, ) self.show_label_action.setCheckable(True) self.show_label_action.setChecked(Conf.view.show_result_label.get()) self.new_action( _("Plot results") + "...", triggered=self.panel.plot_results, icon_name="plot_results.svg", select_condition=SelectCond.at_least_one_group_or_one_object, ) # Create dynamic "Delete results" submenu with self.new_menu(_("Delete results"), icon_name="delete_results.svg"): # Store reference to the submenu for dynamic population if self.__submenu_stack: current_submenu = self.__submenu_stack[-1] self.results_delete_submenu = current_submenu["menu"] # MARK: VIEW with self.new_category(ActionCategory.VIEW): self.new_action( _("Edit annotations") + "...", icon_name="annotations.svg", tip=_("Edit annotations of selected %s") % self.OBJECT_STR, triggered=lambda: self.panel.open_separate_view(edit_annotations=True), context_menu_pos=1, toolbar_pos=-1, ) main = self.panel.mainwindow for cat in (ActionCategory.VIEW, ActionCategory.VIEW_TOOLBAR): for act in (main.autorefresh_action, main.showfirstonly_action): self.add_to_action_list(act, cat, -1) self.new_action( _("Refresh manually"), icon_name="refresh-manual.svg", tip=_("Refresh plot, even if auto-refresh is enabled"), shortcut=QG.QKeySequence(QG.QKeySequence.Refresh), triggered=self.panel.manual_refresh, select_condition=SelectCond.always, toolbar_pos=-1, ) for cat in (ActionCategory.VIEW, ActionCategory.VIEW_TOOLBAR): self.add_to_action_list(main.showlabel_action, cat, -1)
[docs] class SignalActionHandler(BaseActionHandler): """Object handling signal panel GUI interactions: actions, menus, ...""" OBJECT_STR = _("signal") OBJECT_STR_PLURAL = _("signals")
[docs] def create_first_actions(self): """Create actions that are added to the menus in the first place""" super().create_first_actions() # MARK: CREATE with self.new_category(ActionCategory.CREATE): for label, pclass, icon_name, separator in ( (_("Zero"), sio.ZeroParam, "1d-zero.svg", False), ( _("Normal distribution"), sio.NormalDistribution1DParam, "1d-normal.svg", False, ), ( _("Poisson distribution"), sio.PoissonDistribution1DParam, "1d-poisson.svg", False, ), ( _("Uniform distribution"), sio.UniformDistribution1DParam, "1d-uniform.svg", False, ), (_("Gaussian"), sio.GaussParam, "gaussian.svg", True), (_("Lorentzian"), sio.LorentzParam, "lorentzian.svg", False), (_("Voigt"), sio.VoigtParam, "voigt.svg", False), (_("Blackbody (Planck's law)"), sio.PlanckParam, "planck.svg", False), (_("Sine"), sio.SineParam, "sine.svg", True), (_("Cosine"), sio.CosineParam, "cosine.svg", False), (_("Sawtooth"), sio.SawtoothParam, "sawtooth.svg", False), (_("Triangle"), sio.TriangleParam, "triangle.svg", False), (_("Square"), sio.SquareParam, "square.svg", False), (_("Cardinal sine"), sio.SincParam, "sinc.svg", False), (_("Linear chirp"), sio.LinearChirpParam, "linear_chirp.svg", False), (_("Step"), sio.StepParam, "step.svg", True), (_("Exponential"), sio.ExponentialParam, "exponential.svg", False), (_("Logistic"), sio.LogisticParam, "logistic.svg", False), (_("Pulse"), sio.PulseParam, "pulse.svg", False), (_("Step pulse"), sio.StepPulseParam, "step_pulse.svg", False), (_("Square pulse"), sio.SquarePulseParam, "square_pulse.svg", False), (_("Polynomial"), sio.PolyParam, "polynomial.svg", True), (_("Custom"), newobject.CustomSignalParam, None, False), ): self.new_action( label, tip=_("Create new %s") % label, triggered=lambda pclass=pclass: self.panel.new_object(pclass()), icon_name=icon_name, select_condition=SelectCond.always, separator=separator, ) # MARK: OPERATION with self.new_category(ActionCategory.OPERATION): self.action_for("power", separator=True) self.action_for("sqrt") def cra_fit(title, fitdlgfunc, tip: str | None = None): """Create curve fitting action""" return self.new_action( title, triggered=lambda: self.panel.processor.compute_fit(title, fitdlgfunc), icon_name=fitdlgfunc.__name__ + ".svg", tip=tip, ) # MARK: PROCESSING with self.new_category(ActionCategory.PROCESSING): with self.new_menu(_("Axis transformation")): self.action_for("transpose") self.action_for("reverse_x") self.action_for("replace_x_by_other_y") self.action_for("xy_mode") self.action_for("to_cartesian", separator=True) self.action_for("to_polar") with self.new_menu(_("Frequency filters"), icon_name="highpass.svg"): self.action_for("lowpass") self.action_for("highpass") self.action_for("bandpass") self.action_for("bandstop") with self.new_menu(_("Fitting"), icon_name="exponential_fit.svg"): with self.new_menu( _("Interactive fitting"), icon_name="interactive_fit.svg" ): cra_fit(_("Linear fit"), fitdialog.linear_fit) self.new_action( _("Polynomial fit"), triggered=self.panel.processor.compute_polyfit, icon_name="polynomial_fit.svg", ) cra_fit(_("Gaussian fit"), fitdialog.gaussian_fit) cra_fit(_("Lorentzian fit"), fitdialog.lorentzian_fit) cra_fit(_("Voigt fit"), fitdialog.voigt_fit) self.new_action( _("Multi-Gaussian fit"), triggered=self.panel.processor.compute_multigaussianfit, icon_name="multigaussian_fit.svg", ) self.new_action( _("Multi-Lorentzian fit"), triggered=self.panel.processor.compute_multilorentzianfit, icon_name="multilorentzian_fit.svg", ) cra_fit( _("Planckian fit"), fitdialog.planckian_fit, tip=_("Planckian (blackbody radiation) fitting"), ) cra_fit( _("Two half-Gaussian fit"), fitdialog.twohalfgaussian_fit, tip=_("Asymmetric peak fitting with two half-Gaussians"), ) cra_fit( _("Piecewise exponential (raise-decay) fit"), fitdialog.piecewiseexponential_fit, tip=_( "Piecewise exponential fitting with raise and decay " "components" ), ) cra_fit(_("Exponential fit"), fitdialog.exponential_fit) cra_fit(_("Sinusoidal fit"), fitdialog.sinusoidal_fit) cra_fit( _("CDF fit"), fitdialog.cdf_fit, tip=_( "Cumulative distribution function fit, " "related to Error function (erf)" ), ) separator_needed = True for fit_name in ( "linear_fit", "polynomial_fit", "gaussian_fit", "lorentzian_fit", "voigt_fit", "planckian_fit", "twohalfgaussian_fit", "piecewiseexponential_fit", "exponential_fit", "sinusoidal_fit", "cdf_fit", "sigmoid_fit", ): self.action_for(fit_name, separator=separator_needed) separator_needed = False self.action_for("evaluate_fit", separator=True) self.action_for("derivative", separator=True) self.action_for("integral") self.action_for("apply_window", separator=True) self.action_for("detrending") self.action_for("interpolate") self.action_for("resampling") with self.new_menu(_("Stability analysis"), icon_name="stability.svg"): self.action_for("allan_variance") self.action_for("allan_deviation") self.action_for("modified_allan_variance") self.action_for("hadamard_variance") self.action_for("total_variance") self.action_for("time_deviation") self.new_action( _("All stability features") + "...", triggered=self.panel.processor.compute_all_stability, separator=True, tip=_("Compute all stability features"), ) # MARK: ANALYSIS with self.new_category(ActionCategory.ANALYSIS): self.action_for("fwhm") self.action_for("fw1e2") self.new_action( _("Full width at y=..."), triggered=self.panel.processor.compute_full_width_at_y, tip=_("Compute the full width at a given y value"), ) self.action_for("x_at_minmax") self.new_action( _("First abscissa at y=..."), triggered=self.panel.processor.compute_x_at_y, tip=_( "Compute the first abscissa at a given y value " "(linear interpolation)" ), ) self.new_action( _("Ordinate at x=..."), triggered=self.panel.processor.compute_y_at_x, tip=_("Compute the ordinate at a given x value (linear interpolation)"), ) self.action_for("extract_pulse_features") self.new_action( _("Peak detection"), separator=True, triggered=self.panel.processor.compute_peak_detection, icon_name="peak_detect.svg", ) self.action_for("sampling_rate_period", separator=True) self.action_for("dynamic_parameters", context_menu_pos=-1) self.action_for("bandwidth_3db", context_menu_pos=-1) self.action_for("contrast")
[docs] def create_last_actions(self): """Create actions that are added to the menus in the end""" super().create_last_actions() with self.new_category(ActionCategory.OPERATION): self.action_for("signals_to_image", separator=True) with self.new_category(ActionCategory.VIEW): antialiasing_action = self.new_action( _("Curve anti-aliasing"), icon_name="curve_antialiasing.svg", toggled=self.panel.toggle_anti_aliasing, tip=_("Toggle curve anti-aliasing on/off (may slow down plotting)"), toolbar_pos=-1, ) antialiasing_action.setChecked(Conf.view.sig_antialiasing.get(True)) self.new_action( _("Reset curve styles"), select_condition=SelectCond.always, icon_name="reset_curve_styles.svg", triggered=self.panel.reset_curve_styles, tip=_( "Curve styles are looped over a list of predefined styles.\n" "This action resets the list to its initial state." ), toolbar_pos=-1, )
[docs] class ImageActionHandler(BaseActionHandler): """Object handling image panel GUI interactions: actions, menus, ...""" OBJECT_STR = _("image") OBJECT_STR_PLURAL = _("images")
[docs] def create_first_actions(self): """Create actions that are added to the menus in the first place""" # MARK: PROCESSING (1/2) with self.new_category(ActionCategory.PROCESSING): with self.new_menu(_("Geometry"), icon_name="rotate_right.svg"): self.action_for("fliph", context_menu_pos=-1, context_menu_sep=True) self.action_for("transpose", context_menu_pos=-1) self.action_for("flipv", context_menu_pos=-1) self.action_for("rotate270", context_menu_pos=-1) self.action_for("rotate90", context_menu_pos=-1) self.action_for("rotate") self.new_action( _("Distribute on a grid..."), triggered=self.panel.processor.distribute_on_grid, icon_name="distribute_on_grid.svg", select_condition=SelectCond.at_least_two, separator=True, ) self.new_action( _("Reset image positions"), triggered=self.panel.processor.reset_positions, icon_name="reset_positions.svg", select_condition=SelectCond.at_least_two, ) with self.new_menu( _("Axis transformation"), icon_name="axis_transform.svg" ): self.action_for("set_uniform_coords") super().create_first_actions() # MARK: CREATE with self.new_category(ActionCategory.CREATE): for label, pclass, icon_name, separator in ( (_("Zero"), sio.Zero2DParam, "2d-zero.svg", False), ( _("Normal distribution"), sio.NormalDistribution2DParam, "2d-normal.svg", False, ), ( _("Poisson distribution"), sio.PoissonDistribution2DParam, "2d-poisson.svg", False, ), ( _("Uniform distribution"), sio.UniformDistribution2DParam, "2d-uniform.svg", False, ), (_("Gaussian"), sio.Gauss2DParam, "2d-gaussian.svg", True), (_("2D sinc"), sio.Sinc2DParam, "2d-sinc.svg", False), (_("Ring pattern"), sio.Ring2DParam, "ring.svg", True), (_("Ramp"), sio.Ramp2DParam, "2d-ramp.svg", False), (_("Checkerboard"), sio.Checkerboard2DParam, "checkerboard.svg", False), ( _("Sinusoidal grating"), sio.SinusoidalGrating2DParam, "grating.svg", False, ), (_("Siemens star"), sio.SiemensStar2DParam, "siemens.svg", False), ): self.new_action( label, tip=_("Create new %s") % label, triggered=lambda pclass=pclass: self.panel.new_object(pclass()), icon_name=icon_name, select_condition=SelectCond.always, separator=separator, ) # MARK: ROI with self.new_category(ActionCategory.ROI): self.new_action( _("Create ROI grid") + "...", triggered=self.panel.processor.create_roi_grid, icon_name="roi_grid.svg", tip=_("Create a grid of regions of interest"), ) # MARK: OPERATION with self.new_category(ActionCategory.OPERATION): self.action_for("log10_z_plus_n") # MARK: PROCESSING (2/2) with self.new_category(ActionCategory.PROCESSING): with self.new_menu(_("Frequency filters"), icon_name="noise_reduction.svg"): self.action_for("butterworth") self.action_for("gaussian_freq_filter") with self.new_menu(_("Thresholding"), icon_name="thresholding.svg"): self.action_for("threshold") self.action_for("threshold_isodata") self.action_for("threshold_li") self.action_for("threshold_mean") self.action_for("threshold_minimum") self.action_for("threshold_otsu") self.action_for("threshold_triangle") self.action_for("threshold_yen") self.new_action( _("All thresholding methods") + "...", triggered=self.panel.processor.compute_all_threshold, separator=True, tip=_("Apply all thresholding methods"), ) with self.new_menu(_("Exposure"), icon_name="exposure.svg"): self.action_for("adjust_gamma") self.action_for("adjust_log") self.action_for("adjust_sigmoid") self.action_for("equalize_hist") self.action_for("equalize_adapthist") self.action_for("rescale_intensity") with self.new_menu(_("Restoration"), icon_name="noise_reduction.svg"): self.action_for("denoise_tv") self.action_for("denoise_bilateral") self.action_for("denoise_wavelet") self.action_for("denoise_tophat") self.new_action( _("All denoising methods") + "...", triggered=self.panel.processor.compute_all_denoise, separator=True, tip=_("Apply all denoising methods"), ) with self.new_menu(_("Morphology"), icon_name="morphology.svg"): self.action_for("white_tophat") self.action_for("black_tophat") self.action_for("erosion") self.action_for("dilation") self.action_for("opening") self.action_for("closing") self.new_action( _("All morphological operations") + "...", triggered=self.panel.processor.compute_all_morphology, separator=True, tip=_("Apply all morphological operations"), ) with self.new_menu(_("Edge detection"), icon_name="edge_detection.svg"): self.action_for("canny") self.action_for("farid", separator=True) self.action_for("farid_h") self.action_for("farid_v") self.action_for("laplace", separator=True) self.action_for("prewitt", separator=True) self.action_for("prewitt_h") self.action_for("prewitt_v") self.action_for("roberts", separator=True) self.action_for("scharr", separator=True) self.action_for("scharr_h") self.action_for("scharr_v") self.action_for("sobel", separator=True) self.action_for("sobel_h") self.action_for("sobel_v") self.new_action( _("All edge detection filters..."), triggered=self.panel.processor.compute_all_edges, separator=True, tip=_("Compute all edge detection filters"), ) self.new_action( _("Erase area") + "...", triggered=self.panel.processor.compute_erase, icon_name="erase.svg", separator=True, tip=_("Erase area in the image as defined by a region of interest"), ) # MARK: ANALYSIS with self.new_category(ActionCategory.ANALYSIS): with self.new_menu(_("Intensity profiles"), icon_name="profile.svg"): self.new_action( _("Line profile..."), triggered=self.panel.processor.compute_line_profile, icon_name="profile.svg", tip=_("Extract horizontal or vertical profile"), context_menu_pos=-1, context_menu_sep=True, ) self.new_action( _("Segment profile..."), triggered=self.panel.processor.compute_segment_profile, icon_name="profile_segment.svg", tip=_("Extract profile along a segment"), context_menu_pos=-1, ) self.new_action( _("Average profile..."), triggered=self.panel.processor.compute_average_profile, icon_name="profile_average.svg", tip=_("Extract average horizontal or vertical profile"), context_menu_pos=-1, ) self.new_action( _("Radial profile extraction..."), triggered=self.panel.processor.compute_radial_profile, icon_name="profile_radial.svg", tip=_("Radial profile extraction around image centroid"), ) self.action_for("horizontal_projection", separator=True) self.action_for("vertical_projection") self.action_for("centroid", separator=True) self.action_for("enclosing_circle") self.new_action( _("2D peak detection"), separator=True, triggered=self.panel.processor.compute_peak_detection, tip=_("Compute automatic 2D peak detection"), ) self.action_for("contour_shape") self.action_for("hough_circle_peaks") with self.new_menu(_("Blob detection")): self.action_for("blob_dog") self.action_for("blob_doh") self.action_for("blob_log") self.action_for("blob_opencv")
[docs] def create_last_actions(self): """Create actions that are added to the menus in the end""" # MARK: PROCESSING with self.new_category(ActionCategory.PROCESSING): self.new_action( _("Resize") + "...", triggered=self.panel.processor.compute_resize, icon_name="resize.svg", separator=True, ) self.new_action( _("Pixel binning") + "...", triggered=self.panel.processor.compute_binning, icon_name="binning.svg", ) self.action_for("resampling") # MARK: VIEW with self.new_category(ActionCategory.VIEW): self.new_action( _("View images side-by-side") + "...", icon_name="new_window.svg", tip=_("View selected images side-by-side in a new window"), triggered=self.panel.view_images_side_by_side, select_condition=SelectCond.at_least_two, context_menu_pos=-1, ) super().create_last_actions() # MARK: OPERATION with self.new_category(ActionCategory.OPERATION): self.action_for("flatfield", separator=True) # MARK: VIEW with self.new_category(ActionCategory.VIEW): showcontrast_action = self.new_action( _("Show contrast panel"), icon_name="contrast.png", tip=_("Show or hide contrast adjustment panel"), select_condition=SelectCond.always, toggled=self.panel.toggle_show_contrast, toolbar_pos=-1, ) showcontrast_action.setChecked(Conf.view.show_contrast.get(True))