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 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(self):
""" deprecated, see setup_docks_and_widgets
"""
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)