Source code for pymodaq_gui.utils.custom_app

import inspect
from pathlib import Path
from typing import Union, TYPE_CHECKING, Dict, Optional, Iterable

import qt_themes
from qt_themes import Theme
from qtpy.QtCore import QObject, QLocale
from qtpy import QtCore, QtWidgets

from pymodaq_data.h5modules.data_saving import DataToExportSaver
from pymodaq_gui.h5modules.saving import H5Saver
from pymodaq_gui.managers.runner_thread_manager import WorkerThreadManager
from pymodaq_gui.managers.h5manager import FileStatus, H5Manager, FileAction
from pymodaq_utils.config import GlobalConfig as Config
from pymodaq_utils.enums import StrEnum
from pymodaq_utils.logger import set_logger, get_module_name
from pymodaq_utils.config import get_set_path, get_set_local_dir
from pymodaq_utils.warnings import deprecation_msg
from pymodaq_utils.help import get_help_text

from pymodaq_gui.utils.dock import DockArea, Dock
from pymodaq_gui.managers.action_manager import ActionManager
from pymodaq_gui.managers.parameter_manager import ParameterManager
from pymodaq_gui.parameter import ParameterTree
from pymodaq_gui.utils.splash import get_splash_sc

logger = set_logger(get_module_name(__file__))
config = Config()


[docs] class WorkFlowActions(StrEnum): START = 'start' STOP = 'stop' PAUSE = 'pause' LOG = 'log'
[docs] class CustomApp(QObject, ActionManager, ParameterManager): """Base Class to ease the implementation of User Interfaces Inherits the MixIns ActionManager and ParameterManager classes. You have to subclass some methods and make concrete implementation of a given number of methods: * setup_docks_and_widgets: to code the widget layout of your Application using Docks (and the DockArea) or other widgets * setup_menus_and_toolbars: to create the menus and the toolbar associated with actions (see setup_actions) * setup_actions: add actions (see :class:`pymodaq_gui.managers.action_manager.ActionManager`) or widgets and optionally add them to toolbar and menu * connect_things: to connect signals and slots. Either from actions (:meth:`pymodaq_gui.managers.action_manager.ActionManager.connect_action`) or direct signal connection Other methods to reimplement, related to Parameter management * value_changed: non mandatory, see :class:`pymodaq.utils.managers.parameter_manager.ParameterManager` * child_added: non mandatory, see :class:`pymodaq.utils.managers.parameter_manager.ParameterManager` * param_deleted: non mandatory, see :class:`pymodaq.utils.managers.parameter_manager.ParameterManager` Depending on the object type, the mainwindow and dockarea attributes may be None if parent is: * None or QWidget, the attributes will be * parent = QWidget * maindow = None * dockarea = None * DockArea, the attributes will be * parent = DockArea * maindow = QMainWindow * dockarea = DockArea * QMainWindow, the attributes will be * parent = QMainWindow * maindow = QMainWindow * dockarea = None Attributes ---------- title: str Get/set the app title parent: QWidget, QMainWindow or DockArea mainwindow: QMainWindow the parent QMainWindow dockarea: DockArea The underlying DockArea (as central widget of the QMainWindow) menubar: QMenuBar The QMainWindow menubar statusbar: QStatusBar The QMainWindow statusbar splash_sc: QtWidgets.QSplashScreen A splash screen to be used to display information get_theme: method Returns the current QApplication theme, see qt_themes package Parameters ---------- parent: None, QWidget, QMainWindow or DockArea tree: ParameterTree an optional Custom ParameterTree title: str The title of the Application instance toolbar: QTtWidgets.QToolbar a toolbar from another parent application create_app_toolbar: bool If True (default) will create a default toolbar with the name of the application as reference and title add_toolbar_break: bool If True, will add a break in the QToolbarArea before adding the toolbar create_app_menu: bool If True (default is False) will create a default menu in the menubar with the name of the application as reference and title See Also -------- :class:`pymodaq.utils.managers.action_manager.ActionManager`, :class:`pymodaq.utils.managers.parameter_manager.ParameterManager`, """ log_signal = QtCore.Signal(str) show_h5file_statusbar_widgets = False show_workflow_actions = False h5_base_group_name = 'AppData' # rename that in your app/extension to give a meaningful name to your base group params = [] def __init__(self, parent: Union[DockArea, QtWidgets.QMainWindow, QtWidgets.QWidget] = None, tree: ParameterTree = None, title: str = None, toolbar: QtWidgets.QToolBar=None, create_app_toolbar: bool = True, add_toolbar_break=True, create_app_menu: bool = False, h5_actions_not: Iterable[FileAction] = (FileAction.CLOSE_FILE, FileAction.OPEN_FILE)): QObject.__init__(self) ActionManager.__init__(self) ParameterManager.__init__(self, tree=tree) self._splash_sc: Optional[QtWidgets.QSplashScreen] = None if not (isinstance(parent, (DockArea, QtWidgets.QMainWindow, QtWidgets.QWidget))): parent = QtWidgets.QWidget() self.parent = parent if isinstance(parent, DockArea): self.dockarea: DockArea = parent self.mainwindow: QtWidgets.QMainWindow = parent.parent() elif isinstance(parent, QtWidgets.QMainWindow): self.dockarea: DockArea = None self.mainwindow: QtWidgets.QMainWindow = parent else: self.dockarea: DockArea = None self.mainwindow: QtWidgets.QMainWindow = None self._title: str = '' self.title = title # then call self.h5saver property self.docks: Dict[str, Dock] = dict([]) self._menubar: QtWidgets.QMenuBar = None if toolbar is not None: create_app_toolbar = True # force the app toolbar to be the given one if create_app_toolbar: self.add_toolbar(self.__class__.__name__.lower(), self.__class__.__name__, self.mainwindow, toolbar, add_break=add_toolbar_break) self.set_toolbar(toolbar) if self.mainwindow is not None: self.mainwindow.setWindowTitle(self.title) self._menubar = self.mainwindow.menuBar() else: parent.setWindowTitle(self.title) self._statusbar = QtWidgets.QStatusBar() self._status_message_label: QtWidgets.QLabel = None if create_app_menu: self.add_menu(self.__class__.__name__.lower(), self.__class__.__name__, self.menubar if self.mainwindow is not None else None) self._h5_manager = H5Manager(self, show_not=h5_actions_not) self._worker_thread_manager = WorkerThreadManager(parent=self)
[docs] def get_help_markdown(self) -> str: """Markdown text describing how to use this application, shown by the Help action Read from the help.md (or <module>.help.md) next to the module defining the class, else the class docstring """ return get_help_text(self) or inspect.cleandoc(self.__class__.__doc__ or '')
@property def thread_manager(self) -> WorkerThreadManager: return self._worker_thread_manager @property def h5_manager(self) -> H5Manager: return self._h5_manager @property def h5saver(self) -> H5Saver: """ Convenience method to access the h5saver and for backcompatibility""" return self.h5_manager.h5saver
[docs] @classmethod def get_local_folder(cls, user=False) -> Path: """ Create a local User or system wide folder to store things about this extension""" return get_set_path(get_set_local_dir(user=user), cls.__name__)
@property def menubar(self): return self._menubar @property def statusbar(self) -> QtWidgets.QStatusBar | None: return self.mainwindow.statusBar() if self.mainwindow is not None else self._statusbar
[docs] def populate_status_bar(self): """Generic method to populate the Status Bar for customization, reimplement insert_custom_status_widgets method """ self._status_message_label = QtWidgets.QLabel('') self.statusbar.addPermanentWidget(self._status_message_label) self.insert_custom_status_widgets() if self.show_h5file_statusbar_widgets: self.h5_manager.insert_h5stuff_status()
[docs] def set_permanent_status(self, status: str): """ Display a permanent status message Method populate_status_bar should have been called beforehand """ self._status_message_label.setText(status)
[docs] def insert_custom_status_widgets(self): """ create here Widgets to be added to the StatusBar To be reimplemented Examples -------- self._file_open_LED = QLED() self.statusbar.addPermanentWidget(self._file_open_LED) """ pass
[docs] def update_status(self, message: str, wait_time: Optional[int] = None): """Show the message in the status bar with a delay of wait_time ms. """ if self.statusbar is not None: if wait_time is None: wait_time = config('gui', 'message_status_persistence') self.statusbar.showMessage(message, wait_time)
@property def splash_sc(self) -> QtWidgets.QSplashScreen: if not hasattr(self, "_splash_sc") or self._splash_sc is None: self._splash_sc = get_splash_sc() return self._splash_sc @property def title(self) -> str: return self._title @title.setter def title(self, title: str): self._title = title if title is not None else self.__class__.__name__ if self.mainwindow is not None: self.mainwindow.setWindowTitle(self._title)
[docs] @staticmethod def get_theme(name: str = None) -> Theme: return qt_themes.get_theme(name)
[docs] def setup_ui(self): self.setup_docks_and_widgets() self.setup_menus_and_toolbars(self.menubar) # see ActionManager MixIn class if self.show_workflow_actions: self.setup_workflow_actions() self.setup_actions() # see ActionManager MixIn class self.connect_things() self.do_things_after_ui_setup() self.apply_size_hint()
[docs] def quit_fun(self) -> bool | None: """Method to be reimplemented in order to define a custom quit function """ if len(self.thread_manager.worker_threads) > 0: self.thread_manager.exit_worker_threads() if self.mainwindow is not None: self.mainwindow.close() self.disconnect_tree() return True
[docs] def do_things_after_ui_setup(self): """ Method to be reimplemented in order to do things after the UI setup """ pass
[docs] def apply_size_hint(self): if self.mainwindow is not None: self.mainwindow.resize(self._size_hint) else: self.parent.resize(self._size_hint)
@property def _size_hint(self) -> QtCore.QSize: """ property telling the optimal size for your application UI To be reimplemented """ return QtCore.QSize(1200, 800)
[docs] def setup_docks_and_widgets(self): """ Method to be reimplemented to set up the docks layout and/or widgets Examples -------- >>>self.docks['ADock'] = gutils.Dock('ADock name') >>>self.dockarea.addDock(self.docks['ADock']) >>>self.docks['AnotherDock'] = gutils.Dock('AnotherDock name') >>>self.dockarea.addDock(self.docks['AnotherDock'''], 'bottom', self.docks['ADock']) See Also -------- """ if hasattr(self, 'setup_docks'): self.setup_docks() # for backcompatibility deprecation_msg('You should not call setup_docks anymore, use `setup_docks_and_widgets` instead')
[docs] def setup_docks(self): """ deprecated, see setup_docks_and_widgets """ pass
[docs] def setup_menus_and_toolbars(self, menubar: QtWidgets.QMenuBar = None): """Non-mandatory method to be subclassed in order to create menus and toolbars create menu and toolbar for actions defined in setup_actions, for instance: Examples -------- >>>file_menu = self.add_menu('file_menu', 'File', self.menubar) >>>submenu = self.add_menu('submenu', 'ASubMenu', 'file_menu') >>>file_toolbar = self.add_toolbar('file_toolbar', 'File', self.mainwindow) See Also -------- pymodaq.utils.managers.action_manager.ActionManager """ self.setup_menu(menubar) # for back-compatibility
[docs] def setup_menu(self, menubar: QtWidgets.QMenuBar = None): """ Deprecated, use `setup_menus_and_toolbars` """ pass
[docs] def setup_actions(self): """Method where to create actions. To be reimplemented Examples -------- >>> self.add_action('grab', 'Grab', 'camera', "Grab from camera", checkable=True, menu='file_menu') >>> self.add_action('load', 'Load', 'Open', "Load target file (.h5, .png, .jpg) or data from camera", checkable=False) >>> self.add_action('save', 'Save' 'SaveAs', "Save current data", checkable=False) >>>self.affect_to('load', 'file_menu') >>>self.affect_to('save', 'file_menu') """ pass
[docs] def setup_workflow_actions(self): if 'actions' not in self.menus: self.add_menu('actions', 'Actions', parent_menu=self.menubar) self.add_action(WorkFlowActions.START, 'Start Workflow', 'motion_play', "Start the workflow", menu='actions', icon_color=self.get_theme().green) self.add_action(WorkFlowActions.STOP, 'Stop Workflow', 'stop_circle', "Stop the workflow", menu='actions', icon_color=self.get_theme().red) self.add_action(WorkFlowActions.PAUSE, 'Pause Workflow', 'pause_circle', "Pause/resume the workflow", checkable=True, menu='actions', icon_checked_color=self.get_theme().orange) self.toolbar.addSeparator() self.add_action(WorkFlowActions.LOG, 'Do Logging', 'home_storage', tip='Log all data generated within the workflow', menu='actions', icon_checked_color=self.get_theme().green, icon_color=self.get_theme().red, checkable=True, checked=True) self.toolbar.addSeparator()
[docs] def enable_workflow_actions(self, enable=True, excepted: Iterable[str | WorkFlowActions] = (), opposite: Iterable[str | WorkFlowActions] = (), other_actions: Iterable[str | WorkFlowActions] = ()): """ Enable/Disable workflow actions (start, stop, pause) + other specified ones if an action is specified in excepted, nothing is done on it if an action is specified in opposite, the opposite boolean is applied to its enabled status Everytime this function is called the Pause action is unchecked """ if not isinstance(excepted, Iterable): excepted = [excepted] if not isinstance(other_actions, Iterable): other_actions = [other_actions] if not isinstance(opposite, Iterable): opposite = [opposite] for action in [WorkFlowActions(value) for value in WorkFlowActions.values()] + list(other_actions): if self.has_action(action) and action not in excepted: if action in opposite: self.set_action_enabled(action, not enable) else: self.set_action_enabled(action, enable) self.set_action_checked(WorkFlowActions.PAUSE, False)
[docs] def connect_things(self): """Connect actions and/or other widgets signal to methods To be reimplemented """ pass
@property def module_and_data_saver(self) -> DataToExportSaver: return DataToExportSaver(self.h5_manager.h5saver)