Source code for cdl.core.gui.actionhandler

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

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

The :mod:`cdl.core.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 typing import TYPE_CHECKING

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 cdl.config import Conf, _
from cdl.widgets import fitdialog

if TYPE_CHECKING:
    from cdl.core.gui.objectmodel import ObjectGroup
    from cdl.core.gui.panel.image import ImagePanel
    from cdl.core.gui.panel.signal import SignalPanel
    from cdl.core.model.image import ImageObj
    from cdl.core.model.signal import SignalObj


[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 # 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] class ActionCategory(enum.Enum): """Action categories""" FILE = enum.auto() EDIT = enum.auto() VIEW = 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" 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.__actions: dict[Callable, list[QW.QAction]] = {} self.__submenus: dict[str, QW.QMenu] = {} @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 ) -> Generator[None, None, None]: """Context manager for creating a new menu. Args: title: Menu title icon_name: Menu icon name. Defaults to None. Yields: None """ key = self.__category_in_progress.name + "/" + 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)) else: menu = self.__submenus[key] self.__submenu_in_progress = True try: yield finally: self.__submenu_in_progress = False add_actions(menu, self.feature_actions.pop(ActionCategory.SUBMENU)) if is_new: self.add_to_action_list(menu) 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 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: category = ActionCategory.SUBMENU elif 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() 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""" with self.new_category(ActionCategory.FILE): self.new_action( _("New %s...") % self.OBJECT_STR, icon_name=f"new_{self.OBJECT_STR}.svg", tip=_("Create new %s") % self.OBJECT_STR, triggered=self.panel.new_object, shortcut=QG.QKeySequence(QG.QKeySequence.New), select_condition=SelectCond.always, toolbar_pos=-1, ) self.new_action( _("Open %s...") % self.OBJECT_STR, # icon: fileopen_signal.svg or fileopen_image.svg icon_name=f"fileopen_{self.__class__.__name__[:3].lower()}.svg", tip=_("Open %s") % 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( _("Save %s...") % self.OBJECT_STR, # icon: filesave_signal.svg or filesave_image.svg icon_name=f"filesave_{self.__class__.__name__[:3].lower()}.svg", tip=_("Save selected %s") % self.OBJECT_STR, 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( _("Import text file..."), icon_name="import_text.svg", triggered=self.panel.exec_import_wizard, select_condition=SelectCond.always, ) with self.new_category(ActionCategory.EDIT): 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, toolbar_pos=-1, ) self.new_action( _("Rename group..."), icon_name="rename_group.svg", tip=_("Rename selected group"), triggered=self.panel.rename_group, select_condition=SelectCond.exactly_one_group, context_menu_pos=-1, toolbar_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, ) self.new_action( _("Copy metadata"), separator=True, 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, 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"), icon_name="metadata_delete.svg", tip=_("Delete all that is contained in object metadata"), triggered=self.panel.delete_metadata, toolbar_pos=-1, ) self.new_action( _("Add object title to plot"), separator=True, triggered=self.panel.add_label_with_title, tip=_("Add object title as a label to the plot"), ) 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, ) self.new_action( _("Edit regions of interest..."), separator=True, triggered=self.panel.processor.edit_regions_of_interest, icon_name="roi.svg", select_condition=SelectCond.exactly_one, context_menu_pos=-1, context_menu_sep=True, toolbar_pos=-1, toolbar_category=ActionCategory.VIEW_TOOLBAR, ) self.new_action( _("Remove regions of interest"), triggered=self.panel.processor.delete_regions_of_interest, icon_name="roi_delete.svg", select_condition=SelectCond.with_roi, context_menu_pos=-1, ) 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, ) 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) with self.new_category(ActionCategory.OPERATION): self.new_action( _("Sum"), triggered=self.panel.processor.compute_sum, select_condition=SelectCond.at_least_two, icon_name="sum.svg", ) self.new_action( _("Average"), triggered=self.panel.processor.compute_average, select_condition=SelectCond.at_least_two, icon_name="average.svg", ) self.new_action( _("Difference"), triggered=self.panel.processor.compute_difference, select_condition=SelectCond.at_least_one, icon_name="difference.svg", ) self.new_action( _("Quadratic difference"), triggered=self.panel.processor.compute_quadratic_difference, select_condition=SelectCond.at_least_one, icon_name="quadratic_difference.svg", ) self.new_action( _("Product"), triggered=self.panel.processor.compute_product, select_condition=SelectCond.at_least_two, icon_name="product.svg", ) self.new_action( _("Division"), triggered=self.panel.processor.compute_division, select_condition=SelectCond.at_least_one, icon_name="division.svg", ) self.new_action( _("Arithmetic operation") + "...", triggered=self.panel.processor.compute_arithmetic, select_condition=SelectCond.at_least_one, icon_name="arithmetic.svg", ) with self.new_menu(_("Constant Operations"), icon_name="constant.svg"): self.new_action( _("Add constant"), triggered=self.panel.processor.compute_addition_constant, select_condition=SelectCond.at_least_one, icon_name="constant_add.svg", ) self.new_action( _("Substract constant"), triggered=self.panel.processor.compute_difference_constant, select_condition=SelectCond.at_least_one, icon_name="constant_substract.svg", ) self.new_action( _("Multiply by constant"), triggered=self.panel.processor.compute_product_constant, select_condition=SelectCond.at_least_one, icon_name="constant_multiply.svg", ) self.new_action( _("Divide by constant"), triggered=self.panel.processor.compute_division_constant, select_condition=SelectCond.at_least_one, icon_name="constant_divide.svg", ) self.new_action( _("Absolute value"), triggered=self.panel.processor.compute_abs, separator=True, icon_name="abs.svg", ) self.new_action( _("Real part"), triggered=self.panel.processor.compute_re, icon_name="re.svg", ) self.new_action( _("Imaginary part"), triggered=self.panel.processor.compute_im, icon_name="im.svg", ) self.new_action( _("Convert data type"), triggered=self.panel.processor.compute_astype, separator=True, icon_name="convert_dtype.svg", ) self.new_action( _("Exponential"), triggered=self.panel.processor.compute_exp, separator=True, icon_name="exp.svg", ) self.new_action( _("Logarithm (base 10)"), triggered=self.panel.processor.compute_log10, separator=False, icon_name="log10.svg", ) with self.new_category(ActionCategory.PROCESSING): with self.new_menu( _("Axis transformation"), icon_name="axis_transform.svg" ): self.new_action( _("Linear calibration"), triggered=self.panel.processor.compute_calibration, ) self.new_action( _("Swap X/Y axes"), triggered=self.panel.processor.compute_swap_axes, icon_name="swap_x_y.svg", ) with self.new_menu(_("Level adjustment"), icon_name="level_adjustment.svg"): self.new_action( _("Normalize"), triggered=self.panel.processor.compute_normalize, icon_name="normalize.svg", ) self.new_action( _("Clipping"), triggered=self.panel.processor.compute_clip, icon_name="clip.svg", ) 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 reduction"), icon_name="noise_reduction.svg"): self.new_action( _("Gaussian filter"), triggered=self.panel.processor.compute_gaussian_filter, ) self.new_action( _("Moving average"), triggered=self.panel.processor.compute_moving_average, ) self.new_action( _("Moving median"), triggered=self.panel.processor.compute_moving_median, ) self.new_action( _("Wiener filter"), triggered=self.panel.processor.compute_wiener, ) with self.new_menu(_("Fourier analysis"), icon_name="fourier.svg"): self.new_action( _("FFT"), triggered=self.panel.processor.compute_fft, tip=_("Warning: only real part is plotted"), ) self.new_action( _("Inverse FFT"), triggered=self.panel.processor.compute_ifft, tip=_("Warning: only real part is plotted"), ) self.new_action( _("Magnitude spectrum"), triggered=self.panel.processor.compute_magnitude_spectrum, ) self.new_action( _("Phase spectrum"), triggered=self.panel.processor.compute_phase_spectrum, ) self.new_action( _("Power spectral density"), triggered=self.panel.processor.compute_psd, ) with self.new_category(ActionCategory.ANALYSIS): self.new_action( _("Statistics") + "...", triggered=self.panel.processor.compute_stats, icon_name="stats.svg", context_menu_pos=-1, context_menu_sep=True, ) self.new_action( _("Histogram") + "...", triggered=self.panel.processor.compute_histogram, icon_name="histogram.svg", context_menu_pos=-1, ) def create_last_actions(self): """Create actions that are added to the menus in the end""" with self.new_category(ActionCategory.PROCESSING): self.new_action( _("ROI extraction"), triggered=self.panel.processor.compute_roi_extraction, # Icon name is 'signal_roi.svg' or 'image_roi.svg': icon_name=f"{self.OBJECT_STR}_roi.svg", separator=True, ) 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.new_action( _("Plot results") + "...", triggered=self.panel.plot_results, icon_name="plot_results.svg", select_condition=SelectCond.at_least_one_group_or_one_object, ) self.new_action( _("Delete results") + "...", triggered=self.panel.delete_results, icon_name="delete_results.svg", select_condition=SelectCond.at_least_one_group_or_one_object, )
[docs] class SignalActionHandler(BaseActionHandler): """Object handling signal panel GUI interactions: actions, menus, ...""" OBJECT_STR = _("signal")
[docs] def create_first_actions(self): """Create actions that are added to the menus in the first place""" super().create_first_actions() with self.new_category(ActionCategory.OPERATION): self.new_action( _("Power"), triggered=self.panel.processor.compute_power, separator=True, icon_name="power.svg", ) self.new_action( _("Square root"), triggered=self.panel.processor.compute_sqrt, separator=False, icon_name="sqrt.svg", ) self.new_action( _("Derivative"), triggered=self.panel.processor.compute_derivative, separator=True, icon_name="derivative.svg", ) self.new_action( _("Integral"), triggered=self.panel.processor.compute_integral, icon_name="integral.svg", ) def cra_fit(title, fitdlgfunc, iconname, tip: str | None = None): """Create curve fitting action""" return self.new_action( title, triggered=lambda: self.panel.processor.compute_fit(title, fitdlgfunc), icon_name=iconname, tip=tip, ) with self.new_category(ActionCategory.PROCESSING): with self.new_menu(_("Axis transformation")): self.new_action( _("Reverse X-axis"), triggered=self.panel.processor.compute_reverse_x, icon_name="reverse_signal_x.svg", ) with self.new_menu(_("Frequency filters"), icon_name="highpass.svg"): self.new_action( _("Low-pass filter"), triggered=self.panel.processor.compute_lowpass, icon_name="lowpass.svg", ) self.new_action( _("High-pass filter"), triggered=self.panel.processor.compute_highpass, icon_name="highpass.svg", ) self.new_action( _("Band-pass filter"), triggered=self.panel.processor.compute_bandpass, icon_name="bandpass.svg", ) self.new_action( _("Band-stop filter"), triggered=self.panel.processor.compute_bandstop, icon_name="bandstop.svg", ) with self.new_menu(_("Fitting"), icon_name="expfit.svg"): cra_fit(_("Linear fit"), fitdialog.linearfit, "linearfit.svg") self.new_action( _("Polynomial fit"), triggered=self.panel.processor.compute_polyfit, icon_name="polyfit.svg", ) cra_fit(_("Gaussian fit"), fitdialog.gaussianfit, "gaussfit.svg") cra_fit(_("Lorentzian fit"), fitdialog.lorentzianfit, "lorentzfit.svg") cra_fit(_("Voigt fit"), fitdialog.voigtfit, "voigtfit.svg") self.new_action( _("Multi-Gaussian fit"), triggered=self.panel.processor.compute_multigaussianfit, icon_name="multigaussfit.svg", ) cra_fit(_("Exponential fit"), fitdialog.exponentialfit, "expfit.svg") cra_fit(_("Sinusoidal fit"), fitdialog.sinusoidalfit, "sinfit.svg") cra_fit( _("CDF fit"), fitdialog.cdffit, "cdffit.svg", tip=_( "Cumulative distribution function fit, " "related to Error function (erf)" ), ) self.new_action( _("Windowing"), triggered=self.panel.processor.compute_windowing, icon_name="windowing.svg", tip=_( "Apply a window function (or apodization): Hanning, Hamming, ..." ), ) self.new_action( _("Detrending"), triggered=self.panel.processor.compute_detrending, icon_name="detrending.svg", ) self.new_action( _("Interpolation"), triggered=self.panel.processor.compute_interpolation, icon_name="interpolation.svg", ) self.new_action( _("Resampling"), triggered=self.panel.processor.compute_resampling, icon_name="resampling.svg", ) with self.new_category(ActionCategory.ANALYSIS): self.new_action( _("Full width at half-maximum"), triggered=self.panel.processor.compute_fwhm, separator=True, tip=_("Compute Full Width at Half-Maximum (FWHM)"), icon_name="fwhm.svg", ) self.new_action( _("Full width at") + " 1/e²", triggered=self.panel.processor.compute_fw1e2, tip=_("Compute Full Width at Maximum") + "/e²", icon_name="fw1e2.svg", ) self.new_action( _("X values at min/max") + "...", triggered=self.panel.processor.compute_x_at_minmax, tip=_("Compute X values at signal minimum and maximum"), ) self.new_action( _("Peak detection"), separator=True, triggered=self.panel.processor.compute_peak_detection, icon_name="peak_detect.svg", ) self.new_action( _("Sampling rate and period") + "...", separator=True, triggered=self.panel.processor.compute_sampling_rate_period, tip=_( "Compute sampling rate and period for a constant sampling signal" ), ) self.new_action( _("Dynamic parameters") + "...", triggered=self.panel.processor.compute_dynamic_parameters, context_menu_pos=-1, tip=_("Compute dynamic parameters: ENOB, SNR, SINAD, THD, ..."), ) self.new_action( _("Bandwidth at -3dB") + "...", triggered=self.panel.processor.compute_bandwidth_3db, context_menu_pos=-1, tip=_( "Compute bandwidth at -3dB assuming a low-pass filter " "already expressed in dB" ), ) self.new_action( _("Contrast"), triggered=self.panel.processor.compute_contrast, tip=_( "Compute contrast of a signal, i.e. (max-min)/(max+min), " "e.g. for an image profile" ), ) 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] def create_last_actions(self): """Create actions that are added to the menus in the end""" with self.new_category(ActionCategory.OPERATION): self.new_action( _("Convolution"), triggered=self.panel.processor.compute_convolution, separator=True, icon_name="convolution.svg", ) super().create_last_actions()
[docs] class ImageActionHandler(BaseActionHandler): """Object handling image panel GUI interactions: actions, menus, ...""" OBJECT_STR = _("image")
[docs] def create_first_actions(self): """Create actions that are added to the menus in the first place""" super().create_first_actions() 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)) with self.new_category(ActionCategory.OPERATION): self.new_action( "Log10(z+n)", triggered=self.panel.processor.compute_logp1, ) self.new_action( _("Flat-field correction"), separator=True, triggered=self.panel.processor.compute_flatfield, select_condition=SelectCond.at_least_one, ) with self.new_menu(_("Rotation"), icon_name="rotate_right.svg"): self.new_action( _("Flip horizontally"), triggered=self.panel.processor.compute_fliph, icon_name="flip_horizontally.svg", context_menu_pos=-1, context_menu_sep=True, ) self.new_action( _("Flip vertically"), triggered=self.panel.processor.compute_flipv, icon_name="flip_vertically.svg", context_menu_pos=-1, ) self.new_action( _("Rotate %s right") % "90°", # pylint: disable=consider-using-f-string triggered=self.panel.processor.compute_rotate270, icon_name="rotate_right.svg", context_menu_pos=-1, ) self.new_action( _("Rotate %s left") % "90°", # pylint: disable=consider-using-f-string triggered=self.panel.processor.compute_rotate90, icon_name="rotate_left.svg", context_menu_pos=-1, ) self.new_action( _("Rotate arbitrarily..."), triggered=self.panel.processor.compute_rotate, ) 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.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, ) 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_category(ActionCategory.PROCESSING): with self.new_menu(_("Thresholding"), icon_name="thresholding.svg"): self.new_action( _("Parametric thresholding"), triggered=self.panel.processor.compute_threshold, ) self.new_action( _("ISODATA thresholding"), triggered=self.panel.processor.compute_threshold_isodata, ) self.new_action( _("Li thresholding"), triggered=self.panel.processor.compute_threshold_li, ) self.new_action( _("Mean thresholding"), triggered=self.panel.processor.compute_threshold_mean, ) self.new_action( _("Minimum thresholding"), triggered=self.panel.processor.compute_threshold_minimum, ) self.new_action( _("Otsu thresholding"), triggered=self.panel.processor.compute_threshold_otsu, ) self.new_action( _("Triangle thresholding"), triggered=self.panel.processor.compute_threshold_triangle, ) self.new_action( _("Yen thresholding"), triggered=self.panel.processor.compute_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.new_action( _("Gamma correction"), triggered=self.panel.processor.compute_adjust_gamma, ) self.new_action( _("Logarithmic correction"), triggered=self.panel.processor.compute_adjust_log, ) self.new_action( _("Sigmoid correction"), triggered=self.panel.processor.compute_adjust_sigmoid, ) self.new_action( _("Histogram equalization"), triggered=self.panel.processor.compute_equalize_hist, ) self.new_action( _("Adaptive histogram equalization"), triggered=self.panel.processor.compute_equalize_adapthist, ) self.new_action( _("Intensity rescaling"), triggered=self.panel.processor.compute_rescale_intensity, ) with self.new_menu(_("Restoration"), icon_name="noise_reduction.svg"): self.new_action( _("Total variation denoising"), triggered=self.panel.processor.compute_denoise_tv, ) self.new_action( _("Bilateral filter denoising"), triggered=self.panel.processor.compute_denoise_bilateral, ) self.new_action( _("Wavelet denoising"), triggered=self.panel.processor.compute_denoise_wavelet, ) self.new_action( _("White Top-Hat denoising"), triggered=self.panel.processor.compute_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.new_action( _("White Top-Hat (disk)"), triggered=self.panel.processor.compute_white_tophat, ) self.new_action( _("Black Top-Hat (disk)"), triggered=self.panel.processor.compute_black_tophat, ) self.new_action( _("Erosion (disk)"), triggered=self.panel.processor.compute_erosion, ) self.new_action( _("Dilation (disk)"), triggered=self.panel.processor.compute_dilation, ) self.new_action( _("Opening (disk)"), triggered=self.panel.processor.compute_opening, ) self.new_action( _("Closing (disk)"), triggered=self.panel.processor.compute_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(_("Edges"), icon_name="edges.svg"): self.new_action( _("Roberts filter"), triggered=self.panel.processor.compute_roberts ) self.new_action( _("Prewitt filter"), triggered=self.panel.processor.compute_prewitt, separator=True, ) self.new_action( _("Prewitt filter (horizontal)"), triggered=self.panel.processor.compute_prewitt_h, ) self.new_action( _("Prewitt filter (vertical)"), triggered=self.panel.processor.compute_prewitt_v, ) self.new_action( _("Sobel filter"), triggered=self.panel.processor.compute_sobel, separator=True, ) self.new_action( _("Sobel filter (horizontal)"), triggered=self.panel.processor.compute_sobel_h, ) self.new_action( _("Sobel filter (vertical)"), triggered=self.panel.processor.compute_sobel_v, ) self.new_action( _("Scharr filter"), triggered=self.panel.processor.compute_scharr, separator=True, ) self.new_action( _("Scharr filter (horizontal)"), triggered=self.panel.processor.compute_scharr_h, ) self.new_action( _("Scharr filter (vertical)"), triggered=self.panel.processor.compute_scharr_v, ) self.new_action( _("Farid filter"), triggered=self.panel.processor.compute_farid, separator=True, ) self.new_action( _("Farid filter (horizontal)"), triggered=self.panel.processor.compute_farid_h, ) self.new_action( _("Farid filter (vertical)"), triggered=self.panel.processor.compute_farid_v, ) self.new_action( _("Laplace filter"), triggered=self.panel.processor.compute_laplace, separator=True, ) self.new_action( _("All edges filters") + "...", triggered=self.panel.processor.compute_all_edges, separator=True, tip=_("Compute all edges filters"), ) self.new_action( _("Canny filter"), triggered=self.panel.processor.compute_canny ) self.new_action( _("Butterworth filter"), triggered=self.panel.processor.compute_butterworth, ) with self.new_category(ActionCategory.ANALYSIS): # TODO: [P3] Add "Create ROI grid..." action to create a regular grid # or ROIs (maybe reuse/derive from `core.gui.processor.image.GridParam`) self.new_action( _("Centroid"), separator=True, triggered=self.panel.processor.compute_centroid, tip=_("Compute image centroid"), ) self.new_action( _("Minimum enclosing circle center"), triggered=self.panel.processor.compute_enclosing_circle, tip=_("Compute smallest enclosing circle center"), ) self.new_action( _("2D peak detection"), separator=True, triggered=self.panel.processor.compute_peak_detection, tip=_("Compute automatic 2D peak detection"), ) self.new_action( _("Contour detection"), triggered=self.panel.processor.compute_contour_shape, tip=_("Compute contour shape fit"), ) self.new_action( _("Circle Hough transform"), triggered=self.panel.processor.compute_hough_circle_peaks, tip=_("Detect circular shapes using circle Hough transform"), ) with self.new_menu(_("Blob detection")): self.new_action( _("Blob detection (DOG)"), triggered=self.panel.processor.compute_blob_dog, tip=_("Detect blobs using Difference of Gaussian (DOG) method"), ) self.new_action( _("Blob detection (DOH)"), triggered=self.panel.processor.compute_blob_doh, tip=_("Detect blobs using Determinant of Hessian (DOH) method"), ) self.new_action( _("Blob detection (LOG)"), triggered=self.panel.processor.compute_blob_log, tip=_("Detect blobs using Laplacian of Gaussian (LOG) method"), ) self.new_action( _("Blob detection (OpenCV)"), triggered=self.panel.processor.compute_blob_opencv, tip=_("Detect blobs using OpenCV SimpleBlobDetector"), )
[docs] def create_last_actions(self): """Create actions that are added to the menus in the end""" 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", ) super().create_last_actions()