import numbers
from pathlib import Path
from typing import List, Union, Dict, Optional, Tuple, Any
from qtpy import QtWidgets, QtCore
from pymodaq_gui.managers.action_manager import ActionManager
from pymodaq_gui.parameter import Parameter, ParameterTree, ioxml, utils
from pymodaq_gui.utils.file_io import select_file
from pymodaq_utils.config import get_set_config_dir
from pymodaq_utils.logger import set_logger, get_module_name
logger = set_logger(get_module_name(__file__))
[docs]class ParameterManager:
"""Class dealing with Parameter and ParameterTree
Attributes
----------
params: list of dicts
Defining the Parameter tree like structure
settings_name: str
The particular name to give to the object parent Parameter (self.settings)
settings: Parameter
The higher level (parent) Parameter
settings_tree: QWidget
widget Holding a ParameterTree and a toolbar for interacting with the tree
tree: ParameterTree
the underlying ParameterTree
"""
settings_name = 'custom_settings'
params = []
def __init__(self, settings_name: Optional[str] = None,
action_list: tuple = ('save', 'update', 'load'),
):
if settings_name is None:
settings_name = self.settings_name
# create a settings tree to be shown eventually in a dock
# object containing the settings defined in the preamble
# create a settings tree to be shown eventually in a dock
self._settings_tree = ParameterTreeWidget(action_list)
self._settings_tree.get_action(f'save_settings').connect_to(self.save_settings_slot)
self._settings_tree.get_action(f'update_settings').connect_to(self.update_settings_slot)
self._settings_tree.get_action(f'load_settings').connect_to(self.load_settings_slot)
self.settings = Parameter.create(name=settings_name, type='group', children=self.params,
showTop=False) # create a Parameter
# object containing the settings defined in the preamble
self._settings_tree.tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Interactive)
@property
def settings_tree(self):
return self._settings_tree.widget
@property
def tree(self):
return self._settings_tree.tree
@property
def settings(self) -> Parameter:
return self._settings
@settings.setter
def settings(self, settings: Union[Parameter, List[Dict[str, str]], Path]):
settings = self.create_parameter(settings)
self._settings = settings
self.tree.setParameters(self._settings, showTop=False) # load the tree with this parameter object
self._settings.sigTreeStateChanged.connect(self.parameter_tree_changed)
[docs] @staticmethod
def create_parameter(settings: Union[Parameter, List[Dict[str, str]], Path]) -> Parameter:
if isinstance(settings, List):
_settings = Parameter.create(title='Settings', name='settings', type='group',
children=settings, showTop=False)
elif isinstance(settings, Path) or isinstance(settings, str):
settings = Path(settings)
_settings = Parameter.create(title='Settings', name='settings',
type='group', showTop=False,
children=ioxml.XML_file_to_parameter(str(settings)))
elif isinstance(settings, Parameter):
_settings = Parameter.create(title='Settings', name=settings.name(),
type='group', showTop=False)
_settings.restoreState(settings.saveState())
else:
raise TypeError(f'Cannot create Parameter object from {settings}')
return _settings
[docs] def parameter_tree_changed(self, param, changes):
for param, change, data in changes:
path = self._settings.childPath(param)
if change == 'childAdded':
self.child_added(param, data)
elif change == 'value':
self.value_changed(param)
elif change == 'parent':
self.param_deleted(param)
elif change == 'options':
self.options_changed(param, data)
elif change == 'limits':
self.limits_changed(param, data)
[docs] def value_changed(self, param: Parameter):
"""Non-mandatory method to be subclassed for actions to perform (methods to call) when one of the param's
value in self._settings is changed
For this to be triggered, the Parameter method: **setValue** should be used
Parameters
----------
param: Parameter
the parameter whose value just changed
Examples
--------
>>> if param.name() == 'do_something':
>>> if param.value():
>>> print('Do something')
>>> self.settings.child('main_settings', 'something_done').setValue(False)
"""
...
[docs] def child_added(self, param: Parameter, data: Parameter):
"""Non-mandatory method to be subclassed for actions to perform when a param has been
added in the attribute settings
For this to be triggered, one of the Parameter methods: **addChild**, **addChildren**
or **insertChildren** should be used
Parameters
----------
param: Parameter
the parameter where child will be added
data: Parameter
the child parameter
"""
pass
[docs] def param_deleted(self, param: Parameter):
"""Non-mandatory method to be subclassed for actions to perform when one of the param in self.settings has been deleted
For this to be triggered, the Parameter method: **removeChild** should be used
Parameters
----------
param: Parameter
the parameter that has been deleted
"""
pass
[docs] def options_changed(self, param: Parameter, data: Dict[str, Any]):
"""Non-mandatory method to be subclassed for actions to perform when one of the options
of any parameter in the settings attribute has been changed.
For this to be triggered, the Parameter method: **setOpts** should be used
Parameters
----------
param: Parameter
the parameter chose option has been changed
data: dict
the key is the string of the changed option
the value depend on the type of option
"""
pass
[docs] def limits_changed(self, param: Parameter, data: Tuple[numbers.Number, numbers.Number]):
"""Non-mandatory method to be subclassed for actions to perform when the limits
of any parameter in the *settings* attribute has been changed
For this to be triggered, the Parameter method: **setLimits** should be used
Parameters
----------
param: Parameter
the parameter chose option has been changed
data: tuple of numbers
tuple of float or int depending on the parameter type. For peculiar Parameter, could be
a tuple of some other objects
"""
pass
[docs] def save_settings_slot(self, file_path: Path = None):
""" Method to save the current settings using a xml file extension.
The starting directory is the user config folder with a subfolder called settings folder
Parameters
----------
file_path: Path
Path like object pointing to a xml file encoding a Parameter object
If None, opens a file explorer window to save manually a file
"""
if file_path is None or file_path is False:
file_path = select_file(get_set_config_dir('settings', user=True), save=True, ext='xml', filter='*.xml',
force_save_extension=True)
else:
file_path = Path(file_path)
if '.xml' != file_path.suffix:
return
if file_path:
ioxml.parameter_to_xml_file(self.settings, file_path.resolve())
logger.info(f'The settings have been successfully saved at {file_path}')
def _get_settings_from_file(self):
return select_file(get_set_config_dir('settings', user=True), save=False, ext='xml', filter='*.xml',
force_save_extension=True)
[docs] def load_settings_slot(self, file_path: Path = None):
""" Method to load settings into the parameter using a xml file extension.
The starting directory is the user config folder with a subfolder called settings folder
Parameters
----------
file_path: Path
Path like object pointing to a xml file encoding a Parameter object
If None, opens a file explorer window to pick manually a file
"""
if file_path is None or file_path is False:
file_path = self._get_settings_from_file()
if file_path:
self.settings = file_path.resolve()
logger.info(f'The settings from {file_path} have been successfully loaded')
[docs] def update_settings_slot(self, file_path: Path = None):
""" Method to update settings using a xml file extension.
The file should define the same settings structure (names and children)
The starting directory is the user config folder with a subfolder called settings folder
Parameters
----------
file_path: Path
Path like object pointing to a xml file encoding a Parameter object
If None, opens a file explorer window to pick manually a file
"""
if file_path is None or file_path is False:
file_path = self._get_settings_from_file()
if file_path:
_settings = self.create_parameter(file_path.resolve())
# Checking if both parameters have the same structure
sameStruct = utils.compareStructureParameter(self.settings,_settings)
if sameStruct: # Update if true
self.settings = _settings
logger.info(f'The settings from {file_path} have been successfully applied')
else:
logger.info(f'The loaded settings from {file_path} do not match the current settings structure and cannot be applied.')