# 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))