+ Created by Quentin Chateiller, Python drivers originally from
+ Quentin Chateiller and Bruno Garbin, for the C2N-CNRS
+ (Center for Nanosciences and Nanotechnologies, Palaiseau, France)
+ ToniQ team.
+
+ Project continued by Jonathan Peltier, for the C2N-CNRS
+ Minaphot team and Mathieu Jeannin, for the C2N-CNRS
+ Odin team.
+
+
+ Distributed under the terms of the
+ GPL-3.0 licence
+
"""
+ )
+ label_legal.setOpenExternalLinks(True)
+ label_legal.setWordWrap(True)
+ layoutLegal.addWidget(label_legal)
+
+ def closeEvent(self, event):
+ """ Does some steps before the window is really killed """
+ clearAbout()
+
+ if not self.mainGui:
+ QtWidgets.QApplication.quit() # close the about app
diff --git a/autolab/core/gui/GUI_add_device.py b/autolab/core/gui/GUI_add_device.py
new file mode 100644
index 00000000..a6de1ce2
--- /dev/null
+++ b/autolab/core/gui/GUI_add_device.py
@@ -0,0 +1,386 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Aug 5 14:39:02 2024
+
+@author: Jonathan
+"""
+import sys
+
+from qtpy import QtCore, QtWidgets
+
+from .GUI_instances import clearAddDevice
+from .icons import icons
+from ..drivers import (list_drivers, load_driver_lib, get_connection_names,
+ get_driver_class, get_connection_class, get_class_args)
+from ..config import get_all_devices_configs, save_config
+
+
+class AddDeviceWindow(QtWidgets.QMainWindow):
+
+ def __init__(self, parent: QtWidgets.QMainWindow = None):
+
+ super().__init__(parent)
+ self.mainGui = parent
+ self.setWindowTitle('AUTOLAB - Add Device')
+ self.setWindowIcon(icons['autolab'])
+
+ self.statusBar = self.statusBar()
+
+ self._prev_name = ''
+ self._prev_conn = ''
+
+ try:
+ import pyvisa as visa
+ self.rm = visa.ResourceManager()
+ except:
+ self.rm = None
+
+ # Main layout creation
+ layoutWindow = QtWidgets.QVBoxLayout()
+ layoutWindow.setAlignment(QtCore.Qt.AlignTop)
+
+ centralWidget = QtWidgets.QWidget()
+ centralWidget.setLayout(layoutWindow)
+ self.setCentralWidget(centralWidget)
+
+ # Device nickname
+ layoutDeviceNickname = QtWidgets.QHBoxLayout()
+ layoutWindow.addLayout(layoutDeviceNickname)
+
+ label = QtWidgets.QLabel('Device')
+
+ self.deviceNickname = QtWidgets.QLineEdit()
+ self.deviceNickname.setText('my_device')
+
+ layoutDeviceNickname.addWidget(label)
+ layoutDeviceNickname.addWidget(self.deviceNickname)
+
+ # Driver name
+ layoutDriverName = QtWidgets.QHBoxLayout()
+ layoutWindow.addLayout(layoutDriverName)
+
+ label = QtWidgets.QLabel('Driver')
+
+ self.driversComboBox = QtWidgets.QComboBox()
+ self.driversComboBox.addItems(list_drivers())
+ self.driversComboBox.activated.connect(self.driverChanged)
+ self.driversComboBox.setSizeAdjustPolicy(
+ QtWidgets.QComboBox.AdjustToContents)
+
+ layoutDriverName.addWidget(label)
+ layoutDriverName.addStretch()
+ layoutDriverName.addWidget(self.driversComboBox)
+
+ # Driver connection
+ layoutDriverConnection = QtWidgets.QHBoxLayout()
+ layoutWindow.addLayout(layoutDriverConnection)
+
+ label = QtWidgets.QLabel('Connection')
+
+ self.connectionComboBox = QtWidgets.QComboBox()
+ self.connectionComboBox.activated.connect(self.connectionChanged)
+ self.connectionComboBox.setSizeAdjustPolicy(
+ QtWidgets.QComboBox.AdjustToContents)
+
+ layoutDriverConnection.addWidget(label)
+ layoutDriverConnection.addStretch()
+ layoutDriverConnection.addWidget(self.connectionComboBox)
+
+ # Driver arguments
+ self.layoutDriverArgs = QtWidgets.QHBoxLayout()
+ layoutWindow.addLayout(self.layoutDriverArgs)
+
+ self.layoutDriverOtherArgs = QtWidgets.QHBoxLayout()
+ layoutWindow.addLayout(self.layoutDriverOtherArgs)
+
+ # layout for optional args
+ self.layoutOptionalArg = QtWidgets.QVBoxLayout()
+ layoutWindow.addLayout(self.layoutOptionalArg)
+
+ # Add argument
+ layoutButtonArg = QtWidgets.QHBoxLayout()
+ layoutWindow.addLayout(layoutButtonArg)
+
+ addOptionalArg = QtWidgets.QPushButton('Add argument')
+ addOptionalArg.setIcon(icons['add'])
+ addOptionalArg.clicked.connect(lambda state: self.addOptionalArgClicked())
+
+ layoutButtonArg.addWidget(addOptionalArg)
+ layoutButtonArg.setAlignment(QtCore.Qt.AlignLeft)
+
+ # Add device
+ layoutButton = QtWidgets.QHBoxLayout()
+ layoutWindow.addLayout(layoutButton)
+
+ self.addButton = QtWidgets.QPushButton('Add Device')
+ self.addButton.clicked.connect(self.addButtonClicked)
+
+ layoutButton.addWidget(self.addButton)
+
+ # update driver name combobox
+ self.driverChanged()
+
+ self.resize(self.minimumSizeHint())
+
+ def addOptionalArgClicked(self, key: str = None, val: str = None):
+ """ Add new layout for optional argument """
+ layout = QtWidgets.QHBoxLayout()
+ self.layoutOptionalArg.addLayout(layout)
+
+ widget = QtWidgets.QLineEdit()
+ widget.setText(key)
+ layout.addWidget(widget)
+ widget = QtWidgets.QLineEdit()
+ widget.setText(val)
+ layout.addWidget(widget)
+ widget = QtWidgets.QPushButton()
+ widget.setIcon(icons['remove'])
+ widget.clicked.connect(lambda: self.removeOptionalArgClicked(layout))
+ layout.addWidget(widget)
+
+ def removeOptionalArgClicked(self, layout):
+ """ Remove optional argument layout """
+ for j in reversed(range(layout.count())):
+ layout.itemAt(j).widget().setParent(None)
+ layout.setParent(None)
+
+ def addButtonClicked(self):
+ """ Add the device to the config file """
+ device_name = self.deviceNickname.text()
+ driver_name = self.driversComboBox.currentText()
+ conn = self.connectionComboBox.currentText()
+
+ if device_name == '':
+ self.setStatus('Need device name', 10000, False)
+ return None
+
+ device_dict = {}
+ device_dict['driver'] = driver_name
+ device_dict['connection'] = conn
+
+ for layout in (self.layoutDriverArgs, self.layoutDriverOtherArgs):
+ for i in range(0, (layout.count()//2)*2, 2):
+ key = layout.itemAt(i).widget().text()
+ val = layout.itemAt(i+1).widget().text()
+ device_dict[key] = val
+
+ for i in range(self.layoutOptionalArg.count()):
+ layout = self.layoutOptionalArg.itemAt(i).layout()
+ key = layout.itemAt(0).widget().text()
+ val = layout.itemAt(1).widget().text()
+ device_dict[key] = val
+
+ # Update devices config
+ device_config = get_all_devices_configs()
+ new_device = {device_name: device_dict}
+ device_config.update(new_device)
+ save_config('devices_config', device_config)
+
+ if hasattr(self.mainGui, 'initialize'): self.mainGui.initialize()
+
+ self.close()
+
+ def modify(self, nickname: str, conf: dict):
+ """ Modify existing driver (not optimized) """
+
+ self.setWindowTitle('AUTOLAB - Modify device')
+ self.addButton.setText('Modify device')
+
+ self.deviceNickname.setText(nickname)
+ self.deviceNickname.setEnabled(False)
+ driver_name = conf.pop('driver')
+ conn = conf.pop('connection')
+ index = self.driversComboBox.findText(driver_name)
+ self.driversComboBox.setCurrentIndex(index)
+ self.driverChanged()
+
+ try:
+ driver_lib = load_driver_lib(driver_name)
+ except: pass
+ else:
+ list_conn = get_connection_names(driver_lib)
+ if conn not in list_conn:
+ if list_conn:
+ self.setStatus(f"Connection {conn} not found, switch to {list_conn[0]}", 10000, False)
+ conn = list_conn[0]
+ else:
+ self.setStatus(f"No connections available for driver '{driver_name}'", 10000, False)
+ conn = ''
+
+ index = self.connectionComboBox.findText(conn)
+ self.connectionComboBox.setCurrentIndex(index)
+ if index != -1:
+ self.connectionChanged()
+
+ # Used to remove default value
+ try:
+ driver_lib = load_driver_lib(driver_name)
+ driver_class = get_driver_class(driver_lib)
+ assert hasattr(driver_class, 'slot_config')
+ except:
+ slot_config = ''
+ else:
+ slot_config = f'{driver_class.slot_config}'
+
+ # Update args
+ for layout in (self.layoutDriverArgs, self.layoutDriverOtherArgs):
+ for i in range(0, (layout.count()//2)*2, 2):
+ key = layout.itemAt(i).widget().text()
+ if key in conf:
+ layout.itemAt(i+1).widget().setText(conf[key])
+ conf.pop(key)
+
+ # Update optional args
+ for i in reversed(range(self.layoutOptionalArg.count())):
+ layout = self.layoutOptionalArg.itemAt(i).layout()
+ key = layout.itemAt(0).widget().text()
+ val_tmp = layout.itemAt(1).widget().text()
+ # Remove default value
+ if ((key == 'slot1' and val_tmp == slot_config)
+ or (key == 'slot1_name' and val_tmp == 'my_')):
+ for j in reversed(range(layout.count())):
+ layout.itemAt(j).widget().setParent(None)
+ layout.setParent(None)
+ elif key in conf:
+ layout.itemAt(1).widget().setText(conf[key])
+ conf.pop(key)
+
+ # Add remaining optional args from config
+ for key, val in conf.items():
+ self.addOptionalArgClicked(key, val)
+
+ def driverChanged(self):
+ """ Update driver information """
+ driver_name = self.driversComboBox.currentText()
+
+ if driver_name == self._prev_name: return None
+ if driver_name == '':
+ self.setStatus(f"Can't load driver associated with {self.deviceNickname.text()}", 10000, False)
+ return None
+ self._prev_name = driver_name
+
+ try:
+ driver_lib = load_driver_lib(driver_name)
+ except Exception as e:
+ # If error with driver remove all layouts
+ self.setStatus(f"Can't load {driver_name}: {e}", 10000, False)
+
+ self.connectionComboBox.clear()
+
+ for layout in (self.layoutDriverArgs, self.layoutDriverOtherArgs):
+ for i in reversed(range(layout.count())):
+ layout.itemAt(i).widget().setParent(None)
+
+ for i in reversed(range(self.layoutOptionalArg.count())):
+ layout = self.layoutOptionalArg.itemAt(i).layout()
+ for j in reversed(range(layout.count())):
+ layout.itemAt(j).widget().setParent(None)
+ layout.setParent(None)
+
+ return None
+
+ self.setStatus('')
+
+ # Update available connections
+ connections = get_connection_names(driver_lib)
+ self.connectionComboBox.clear()
+ self.connectionComboBox.addItems(connections)
+
+ # update selected connection information
+ self._prev_conn = ''
+ self.connectionChanged()
+
+ # reset layoutDriverOtherArgs
+ for i in reversed(range(self.layoutDriverOtherArgs.count())):
+ self.layoutDriverOtherArgs.itemAt(i).widget().setParent(None)
+
+ # used to skip doublon key
+ conn = self.connectionComboBox.currentText()
+ try:
+ driver_instance = get_connection_class(driver_lib, conn)
+ except:
+ connection_args = {}
+ else:
+ connection_args = get_class_args(driver_instance)
+
+ # populate layoutDriverOtherArgs
+ driver_class = get_driver_class(driver_lib)
+ other_args = get_class_args(driver_class)
+ for key, val in other_args.items():
+ if key in connection_args: continue
+ widget = QtWidgets.QLabel()
+ widget.setText(key)
+ self.layoutDriverOtherArgs.addWidget(widget)
+ widget = QtWidgets.QLineEdit()
+ widget.setText(str(val))
+ self.layoutDriverOtherArgs.addWidget(widget)
+
+ # reset layoutOptionalArg
+ for i in reversed(range(self.layoutOptionalArg.count())):
+ layout = self.layoutOptionalArg.itemAt(i).layout()
+ for j in reversed(range(layout.count())):
+ layout.itemAt(j).widget().setParent(None)
+ layout.setParent(None)
+
+ # populate layoutOptionalArg
+ if hasattr(driver_class, 'slot_config'):
+ self.addOptionalArgClicked('slot1', f'{driver_class.slot_config}')
+ self.addOptionalArgClicked('slot1_name', 'my_')
+
+ def connectionChanged(self):
+ """ Update connection information """
+ conn = self.connectionComboBox.currentText()
+
+ if conn == self._prev_conn: return None
+ self._prev_conn = conn
+
+ driver_name = self.driversComboBox.currentText()
+ try:
+ driver_lib = load_driver_lib(driver_name)
+ except:
+ return None
+
+ connection_args = get_class_args(
+ get_connection_class(driver_lib, conn))
+
+ # reset layoutDriverArgs
+ for i in reversed(range(self.layoutDriverArgs.count())):
+ self.layoutDriverArgs.itemAt(i).widget().setParent(None)
+
+ conn_widget = None
+ # populate layoutDriverArgs
+ for key, val in connection_args.items():
+ widget = QtWidgets.QLabel()
+ widget.setText(key)
+ self.layoutDriverArgs.addWidget(widget)
+
+ widget = QtWidgets.QLineEdit()
+ widget.setText(str(val))
+ self.layoutDriverArgs.addWidget(widget)
+
+ if key == 'address':
+ conn_widget = widget
+
+ if self.rm is not None and conn == 'VISA':
+ widget = QtWidgets.QComboBox()
+ widget.clear()
+ conn_list = ('Available connections',) + tuple(self.rm.list_resources())
+ widget.addItems(conn_list)
+ if conn_widget is not None:
+ widget.activated.connect(
+ lambda item, conn_widget=conn_widget: conn_widget.setText(
+ widget.currentText()) if widget.currentText(
+ ) != 'Available connections' else conn_widget.text())
+ self.layoutDriverArgs.addWidget(widget)
+
+ def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
+ """ Modify the message displayed in the status bar and add error message to logger """
+ self.statusBar.showMessage(message, timeout)
+ if not stdout: print(message, file=sys.stderr)
+
+ def closeEvent(self, event):
+ """ Does some steps before the window is really killed """
+ clearAddDevice()
+
+ if not self.mainGui:
+ QtWidgets.QApplication.quit() # close the monitor app
diff --git a/autolab/core/gui/GUI_driver_installer.py b/autolab/core/gui/GUI_driver_installer.py
new file mode 100644
index 00000000..5f147c80
--- /dev/null
+++ b/autolab/core/gui/GUI_driver_installer.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Sep 25 10:46:14 2024
+
+@author: jonathan
+"""
+import sys
+
+from qtpy import QtWidgets, QtGui
+
+from .icons import icons
+from .GUI_instances import clearDriverInstaller
+from ..paths import DRIVER_SOURCES, DRIVER_REPOSITORY
+from ..repository import install_drivers, _download_driver, _get_drivers_list_from_github
+from ..drivers import update_drivers_paths
+
+
+class DriverInstaller(QtWidgets.QMainWindow):
+
+ def __init__(self, parent=None):
+ """ GUI to select which driver to install from the official github repo """
+
+ super().__init__()
+
+ self.setWindowTitle("AUTOLAB - Driver Installer")
+ self.setWindowIcon(icons['autolab'])
+ self.setFocus()
+ self.activateWindow()
+
+ self.statusBar = self.statusBar()
+
+ official_folder = DRIVER_SOURCES['official']
+ official_url = DRIVER_REPOSITORY[official_folder]
+
+ list_driver = []
+ try:
+ list_driver = _get_drivers_list_from_github(official_url)
+ except:
+ self.setStatus(f'Warning: Cannot access {official_url}', 10000, False)
+
+ self.mainGui = parent
+ self.url = official_url # TODO: use dict DRIVER_REPOSITORY to have all urls
+ self.list_driver = list_driver
+ self.OUTPUT_DIR = official_folder
+
+ self.init_ui()
+
+ self.adjustSize()
+
+ def init_ui(self):
+ centralWidget = QtWidgets.QWidget()
+ self.setCentralWidget(centralWidget)
+
+ # OFFICIAL DRIVERS
+ formLayout = QtWidgets.QFormLayout()
+ centralWidget.setLayout(formLayout)
+
+ self.masterCheckBox = QtWidgets.QCheckBox(f"From {DRIVER_REPOSITORY[DRIVER_SOURCES['official']]}:")
+ self.masterCheckBox.setChecked(False)
+ self.masterCheckBox.stateChanged.connect(self.masterCheckBoxChanged)
+ formLayout.addRow(self.masterCheckBox)
+
+ # Init table size
+ sti = QtGui.QStandardItemModel()
+ for i in range(len(self.list_driver)):
+ sti.appendRow([QtGui.QStandardItem(str())])
+
+ # Create table
+ tab = QtWidgets.QTableView()
+ tab.setModel(sti)
+ tab.verticalHeader().setVisible(False)
+ tab.horizontalHeader().setVisible(False)
+ tab.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ tab.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+ tab.setAlternatingRowColors(True)
+ tab.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding)
+ tab.setSizeAdjustPolicy(
+ QtWidgets.QAbstractScrollArea.AdjustToContents)
+
+ if self.list_driver: # OPTIMIZE: c++ crash if no driver in list!
+ tab.horizontalHeader().setSectionResizeMode(
+ 0, QtWidgets.QHeaderView.ResizeToContents)
+
+ # Init checkBox
+ self.list_checkBox = []
+ for i, driver_name in enumerate(self.list_driver):
+ checkBox = QtWidgets.QCheckBox(f"{driver_name}")
+ checkBox.setChecked(False)
+ self.list_checkBox.append(checkBox)
+ tab.setIndexWidget(sti.index(i, 0), checkBox)
+
+ formLayout.addRow(QtWidgets.QLabel(""), tab)
+
+ download_pushButton = QtWidgets.QPushButton()
+ download_pushButton.clicked.connect(self.installListDriver)
+ download_pushButton.setText("Download")
+ formLayout.addRow(download_pushButton)
+
+ def masterCheckBoxChanged(self):
+ """ Checked all the checkBox related to the official github repo """
+ state = self.masterCheckBox.isChecked()
+ for checkBox in self.list_checkBox:
+ checkBox.setChecked(state)
+
+ def installListDriver(self):
+ """ Install all the drivers for which the corresponding checkBox has been checked """
+ list_bool = [
+ checkBox.isChecked() for checkBox in self.list_checkBox]
+ list_driver_to_download = [
+ driver_name for (driver_name, driver_bool) in zip(
+ self.list_driver, list_bool) if driver_bool]
+
+ try:
+ # Better for all drivers
+ if all(list_bool):
+ install_drivers(skip_input=True)
+ # Better for several drivers
+ elif any(list_bool):
+ for driver_name in list_driver_to_download:
+ # self.setStatus(f"Downloading {driver_name}", 5000) # OPTIMIZE: currently thread blocked by installer so don't show anything until the end
+ e = _download_driver(
+ self.url, driver_name, self.OUTPUT_DIR, _print=False)
+ if e is not None:
+ print(e, file=sys.stderr)
+ # self.setStatus(e, 10000, False)
+ except Exception as e:
+ self.setStatus(f'Error: {e}', 10000, False)
+ else:
+ self.setStatus('Finished!', 5000)
+
+ # Update available drivers
+ update_drivers_paths()
+
+ def closeEvent(self, event):
+ """ This function does some steps before the window is really killed """
+ clearDriverInstaller()
+
+ super().closeEvent(event)
+
+ if not self.mainGui:
+ QtWidgets.QApplication.quit() # close the app
+
+ def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
+ """ Modify the message displayed in the status bar and add error message to logger """
+ self.statusBar.showMessage(message, timeout)
+ if not stdout: print(message, file=sys.stderr)
diff --git a/autolab/core/gui/GUI_instances.py b/autolab/core/gui/GUI_instances.py
new file mode 100644
index 00000000..7b0b20e3
--- /dev/null
+++ b/autolab/core/gui/GUI_instances.py
@@ -0,0 +1,336 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Sat Aug 3 20:40:00 2024
+
+@author: jonathan
+"""
+
+from typing import Union, Any
+
+import pandas as pd
+from qtpy import QtWidgets, QtCore
+
+from ..devices import get_final_device_config
+
+from ..elements import Variable as Variable_og
+from ..variables import Variable
+
+# Contains local import:
+# from .monitoring.main import Monitor
+# from .GUI_slider import Slider
+# from .GUI_variables import VariablesMenu
+# from .plotting.main import Plotter
+# from .GUI_add_device import AddDeviceWindow
+# from .GUI_about import AboutWindow
+# Not yet or maybe never (too intertwined with mainGui) # from .scanning.main import Scanner
+
+
+instances = {
+ 'monitors': {},
+ 'sliders': {},
+ 'variablesMenu': None,
+ 'plotter': None,
+ 'addDevice': None,
+ 'about': None,
+ 'preferences': None,
+ 'driverInstaller': None,
+ # 'scanner': None,
+}
+
+
+# =============================================================================
+# Monitor
+# =============================================================================
+def openMonitor(variable: Union[Variable, Variable_og],
+ has_parent: bool = False):
+ """ Opens the monitor associated to the variable. """
+ from .monitoring.main import Monitor # Inside to avoid circular import
+
+ assert isinstance(variable, (Variable, Variable_og)), (
+ f'Need type {Variable} or {Variable_og}, but given type is {type(variable)}')
+ assert variable.readable, f"The variable {variable.address()} is not readable"
+
+ # If the monitor is not already running, create one
+ if id(variable) not in instances['monitors'].keys():
+ instances['monitors'][id(variable)] = Monitor(variable, has_parent)
+ instances['monitors'][id(variable)].show()
+ # If the monitor is already running, just make as the front window
+ else:
+ monitor = instances['monitors'][id(variable)]
+ monitor.setWindowState(
+ monitor.windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ monitor.activateWindow()
+
+
+def clearMonitor(variable: Union[Variable, Variable_og]):
+ """ Clears monitor instances reference when quitted """
+ if id(variable) in list(instances['monitors']):
+ instances['monitors'].pop(id(variable))
+
+
+def closeMonitors():
+ for monitor in list(instances['monitors'].values()):
+ monitor.close()
+
+
+# =============================================================================
+# Slider
+# =============================================================================
+def openSlider(variable: Union[Variable, Variable_og],
+ gui: QtWidgets.QMainWindow = None,
+ item: QtWidgets.QTreeWidgetItem = None):
+ """ Opend the slider associated to this variable. """
+ from .GUI_slider import Slider # Inside to avoid circular import
+
+ assert isinstance(variable, (Variable, Variable_og)), (
+ f'Need type {Variable} or {Variable_og}, but given type is {type(variable)}')
+ assert variable.writable, f"The variable {variable.address()} is not writable"
+
+ # If the slider is not already running, create one
+ if id(variable) not in instances['sliders'].keys():
+ instances['sliders'][id(variable)] = Slider(variable, gui=gui, item=item)
+ instances['sliders'][id(variable)].show()
+ # If the slider is already running, just make as the front window
+ else:
+ slider = instances['sliders'][id(variable)]
+ slider.setWindowState(
+ slider.windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ slider.activateWindow()
+
+
+def clearSlider(variable: Union[Variable, Variable_og]):
+ """ Clears the slider instances reference when quitted """
+ if id(variable) in instances['sliders'].keys():
+ instances['sliders'].pop(id(variable))
+
+
+def closeSliders():
+ for slider in list(instances['sliders'].values()):
+ slider.close()
+
+
+# =============================================================================
+# VariableMenu
+# =============================================================================
+def openVariablesMenu(has_parent: bool = False):
+ """ Opens the variables menu. """
+ from .GUI_variables import VariablesMenu # Inside to avoid circular import
+ if instances['variablesMenu'] is None:
+ instances['variablesMenu'] = VariablesMenu(has_parent)
+ instances['variablesMenu'].show()
+ else:
+ instances['variablesMenu'].refresh()
+ instances['variablesMenu'].setWindowState(
+ instances['variablesMenu'].windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ instances['variablesMenu'].activateWindow()
+
+
+def clearVariablesMenu():
+ """ Clears the variables menu instance reference when quitted """
+ instances['variablesMenu'] = None
+
+
+def closeVariablesMenu():
+ if instances['variablesMenu'] is not None:
+ instances['variablesMenu'].close()
+
+
+# =============================================================================
+# Plotter
+# =============================================================================
+def openPlotter(variable: Union[Variable, Variable_og, pd.DataFrame, Any] = None,
+ has_parent: bool = False):
+ """ Opens the plotter. Can add variable. """
+ from .plotting.main import Plotter # Inside to avoid circular import
+ # If the plotter is not already running, create one
+ if instances['plotter'] is None:
+ instances['plotter'] = Plotter(has_parent)
+ # If the plotter is not active open it (keep data if closed)
+ if not instances['plotter'].active:
+ instances['plotter'].show()
+ instances['plotter'].activateWindow()
+ instances['plotter'].active = True
+ # If the plotter is already running, just make as the front window
+ else:
+ instances['plotter'].setWindowState(
+ instances['plotter'].windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ instances['plotter'].activateWindow()
+
+ if variable is not None:
+ instances['plotter'].refreshPlotData(variable)
+
+
+def clearPlotter():
+ """ Deactivates the plotter when quitted but keep the instance in memory """
+ if instances['plotter'] is not None:
+ instances['plotter'].active = False # don't want to close plotter because want to keep data
+
+
+def closePlotter():
+ if instances['plotter'] is not None:
+ instances['plotter'].figureManager.fig.deleteLater()
+ for children in instances['plotter'].findChildren(QtWidgets.QWidget):
+ children.deleteLater()
+
+ instances['plotter'].close()
+ instances['plotter'] = None # To remove plotter from memory
+
+
+# =============================================================================
+# AddDevice
+# =============================================================================
+def openAddDevice(gui: QtWidgets.QMainWindow = None, name: str = ''):
+ """ Opens the add device window. """
+ from .GUI_add_device import AddDeviceWindow # Inside to avoid circular import
+ # If the add device window is not already running, create one
+ if instances['addDevice'] is None:
+ instances['addDevice'] = AddDeviceWindow(gui)
+ instances['addDevice'].show()
+ instances['addDevice'].activateWindow()
+ # If the add device window is already running, just make as the front window
+ else:
+ instances['addDevice'].setWindowState(
+ instances['addDevice'].windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ instances['addDevice'].activateWindow()
+
+ # Modify existing device
+ if name != '':
+ try:
+ conf = get_final_device_config(name)
+ except Exception as e:
+ instances['addDevice'].setStatus(str(e), 10000, False)
+ else:
+ instances['addDevice'].modify(name, conf)
+
+
+def clearAddDevice():
+ """ Clears the addDevice instance reference when quitted """
+ instances['addDevice'] = None
+
+
+def closeAddDevice():
+ if instances['addDevice'] is not None:
+ instances['addDevice'].close()
+
+
+# =============================================================================
+# About
+# =============================================================================
+def openAbout(gui: QtWidgets.QMainWindow = None):
+ """ Opens the about window. """
+ # If the about window is not already running, create one
+ from .GUI_about import AboutWindow # Inside to avoid circular import
+ if instances['about'] is None:
+ instances['about'] = AboutWindow(gui)
+ instances['about'].show()
+ instances['about'].activateWindow()
+ # If the about window is already running, just make as the front window
+ else:
+ instances['about'].setWindowState(
+ instances['about'].windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ instances['about'].activateWindow()
+
+
+def clearAbout():
+ """ Clears the about instance reference when quitted """
+ instances['about'] = None
+
+
+def closeAbout():
+ if instances['about'] is not None:
+ instances['about'].close()
+
+
+# =============================================================================
+# Preferences
+# =============================================================================
+def openPreferences(gui: QtWidgets.QMainWindow = None):
+ """ Opens the preferences window. """
+ # If the about window is not already running, create one
+ from .GUI_preferences import PreferencesWindow # Inside to avoid circular import
+ if instances['preferences'] is None:
+ instances['preferences'] = PreferencesWindow(gui)
+ instances['preferences'].show()
+ instances['preferences'].activateWindow()
+ # If the about window is already running, just make as the front window
+ else:
+ instances['preferences'].setWindowState(
+ instances['preferences'].windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ instances['preferences'].activateWindow()
+
+
+def clearPreferences():
+ """ Clears the preferences instance reference when quitted """
+ instances['preferences'] = None
+
+
+def closePreferences():
+ if instances['preferences'] is not None:
+ instances['preferences'].close()
+
+
+
+# =============================================================================
+# Driver installer
+# =============================================================================
+def openDriverInstaller(gui: QtWidgets.QMainWindow = None):
+ """ Opens the driver installer. """
+ # If the about window is not already running, create one
+ from .GUI_driver_installer import DriverInstaller # Inside to avoid circular import
+ if instances['driverInstaller'] is None:
+ instances['driverInstaller'] = DriverInstaller(gui)
+ instances['driverInstaller'].show()
+ instances['driverInstaller'].activateWindow()
+ # If the about window is already running, just make as the front window
+ else:
+ instances['driverInstaller'].setWindowState(
+ instances['driverInstaller'].windowState()
+ & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+ instances['driverInstaller'].activateWindow()
+
+
+def clearDriverInstaller():
+ """ Clears the driver unstaller instance reference when quitted """
+ instances['driverInstaller'] = None
+
+
+def closeDriverInstaller():
+ if instances['driverInstaller'] is not None:
+ instances['driverInstaller'].close()
+
+
+# =============================================================================
+# Scanner
+# =============================================================================
+# def openScanner(gui: QtWidgets.QMainWindow, show=True):
+# """ Opens the scanner. """
+# # If the scanner is not already running, create one
+# from .scanning.main import Scanner # Inside to avoid circular import
+# if instances['scanner'] is None:
+# instances['scanner'] = Scanner(gui)
+# instances['scanner'].show()
+# instances['scanner'].activateWindow()
+# gui.activateWindow() # Put main window back to the front
+# # If the scanner is already running, just make as the front window
+# elif show:
+# instances['scanner'].setWindowState(
+# instances['scanner'].windowState()
+# & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+# instances['scanner'].activateWindow()
+
+
+# def clearScanner():
+# """ Clears the scanner instance reference when quitted """
+# instances['scanner'] = None
+
+
+# def closeScanner():
+# if instances['scanner'] is not None:
+# instances['scanner'].close()
diff --git a/autolab/core/gui/GUI_preferences.py b/autolab/core/gui/GUI_preferences.py
new file mode 100644
index 00000000..a6296e40
--- /dev/null
+++ b/autolab/core/gui/GUI_preferences.py
@@ -0,0 +1,524 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Sep 17 22:56:32 2024
+
+@author: jonathan
+"""
+
+from typing import Dict
+import sys
+
+from qtpy import QtCore, QtWidgets
+
+from .icons import icons
+from .GUI_instances import clearPreferences
+from ..utilities import boolean
+from ..config import (check_autolab_config, change_autolab_config,
+ check_plotter_config, change_plotter_config,
+ load_config, modify_config, set_temp_folder)
+
+
+class PreferencesWindow(QtWidgets.QMainWindow):
+
+ def __init__(self, parent: QtWidgets.QMainWindow = None):
+
+ super().__init__()
+ self.mainGui = parent
+ self.setWindowTitle('AUTOLAB - Preferences')
+ self.setWindowIcon(icons['preference'])
+
+ self.setFocus()
+ self.activateWindow()
+ self.statusBar = self.statusBar()
+
+ self.init_ui()
+
+ self.adjustSize()
+
+ self.resize(500, 670)
+
+ def init_ui(self):
+ # Update config if needed
+ check_autolab_config()
+ check_plotter_config()
+
+ autolab_config = load_config('autolab_config')
+ plotter_config = load_config('plotter_config')
+
+ centralWidget = QtWidgets.QWidget()
+ layout = QtWidgets.QVBoxLayout(centralWidget)
+ layout.setContentsMargins(0,0,0,0)
+ layout.setSpacing(0)
+ self.setCentralWidget(centralWidget)
+
+ scrollAutolab = QtWidgets.QScrollArea()
+ scrollAutolab.setWidgetResizable(True)
+ frameAutolab = QtWidgets.QFrame()
+ scrollAutolab.setWidget(frameAutolab)
+ layoutAutolab = QtWidgets.QVBoxLayout(frameAutolab)
+ layoutAutolab.setAlignment(QtCore.Qt.AlignTop)
+
+ scrollPlotter = QtWidgets.QScrollArea()
+ scrollPlotter.setWidgetResizable(True)
+ framePlotter = QtWidgets.QFrame()
+ scrollPlotter.setWidget(framePlotter)
+ layoutPlotter = QtWidgets.QVBoxLayout(framePlotter)
+ layoutPlotter.setAlignment(QtCore.Qt.AlignTop)
+
+ tab = QtWidgets.QTabWidget(self)
+ tab.addTab(scrollAutolab, 'Autolab')
+ tab.addTab(scrollPlotter, 'Plotter')
+
+ layout.addWidget(tab)
+
+ # Create a frame for each main key in the dictionary
+ self.inputs_autolab = {}
+ self.inputs_plotter = {}
+
+ ### Autolab
+ ## GUI
+ main_key = 'GUI'
+ group_box = QtWidgets.QGroupBox(main_key)
+ layoutAutolab.addWidget(group_box)
+ group_layout = QtWidgets.QFormLayout(group_box)
+ self.inputs_autolab[main_key] = {}
+
+ sub_key = 'qt_api'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QComboBox()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the GUI Qt binding')
+ input_widget.addItems(['default', 'pyqt5', 'pyqt6', 'pyside2', 'pyside6'])
+ index = input_widget.findText(saved_value)
+ input_widget.setCurrentIndex(index)
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ sub_key = 'theme'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QComboBox()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the GUI theme')
+ input_widget.addItems(['default', 'dark'])
+ index = input_widget.findText(saved_value)
+ input_widget.setCurrentIndex(index)
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ sub_key = 'font_size'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QSpinBox()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the GUI text font size')
+ input_widget.setValue(int(float(saved_value)))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ sub_key = 'image_background'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QLineEdit()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the plots background color (disabled if use dark theme)')
+ input_widget.setText(str(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ sub_key = 'image_foreground'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QLineEdit()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the plots foreground color (disabled if use dark theme)')
+ input_widget.setText(str(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ ## control_center
+ main_key = 'control_center'
+ group_box = QtWidgets.QGroupBox(main_key)
+ layoutAutolab.addWidget(group_box)
+ group_layout = QtWidgets.QFormLayout(group_box)
+ self.inputs_autolab[main_key] = {}
+
+ sub_key = 'precision'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QSpinBox()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the displayed precision for variables in the control panel')
+ input_widget.setValue(int(float(saved_value)))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ sub_key = 'print'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select if print GUI information to console')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ sub_key = 'logger'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Activate a logger to display GUI information')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ sub_key = 'console'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Activate a console for debugging')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ ## monitor
+ main_key = 'monitor'
+ group_box = QtWidgets.QGroupBox(main_key)
+ layoutAutolab.addWidget(group_box)
+ group_layout = QtWidgets.QFormLayout(group_box)
+ self.inputs_autolab[main_key] = {}
+
+ sub_key = 'precision'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QSpinBox()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the displayed precision for variables in monitors')
+ input_widget.setValue(int(float(saved_value)))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ sub_key = 'save_figure'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select if save figure image when saving monitor data')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ ## scanner
+ main_key = 'scanner'
+ group_box = QtWidgets.QGroupBox(main_key)
+ layoutAutolab.addWidget(group_box)
+ group_layout = QtWidgets.QFormLayout(group_box)
+ self.inputs_autolab[main_key] = {}
+
+ sub_key = 'precision'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QSpinBox()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the displayed precision for variables in the scanner')
+ input_widget.setValue(int(float(saved_value)))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ sub_key = 'save_config'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select if save config file when saving scanner data')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ sub_key = 'save_figure'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select if save figure image when saving scanner data')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ sub_key = 'save_temp'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select if save temporary data. Should not be disable!')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ sub_key = 'ask_close'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QCheckBox(sub_key)
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Set whether a warning message about unsaved data should be displayed when the scanner is closed.')
+ input_widget.setChecked(boolean(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(input_widget)
+
+ ## directories
+ main_key = 'directories'
+ group_box = QtWidgets.QGroupBox(main_key)
+ layoutAutolab.addWidget(group_box)
+ group_layout = QtWidgets.QFormLayout(group_box)
+ self.inputs_autolab[main_key] = {}
+
+ sub_key = 'temp_folder'
+ saved_value = autolab_config[main_key][sub_key]
+ input_widget = QtWidgets.QLineEdit()
+ input_widget.setToolTip("Select the temporary folder. default correspond to os.environ['TEMP']")
+ input_widget.setText(str(saved_value))
+ self.inputs_autolab[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ ## extra_driver_path
+ main_key = 'extra_driver_path'
+ group_box = QtWidgets.QFrame()
+ group_layout = QtWidgets.QVBoxLayout(group_box)
+ group_layout.setContentsMargins(0,0,0,0)
+ layoutAutolab.addWidget(group_box)
+ group_box_plugin = QtWidgets.QGroupBox(main_key)
+ group_layout.addWidget(group_box_plugin)
+ group_box.setToolTip('Add extra driver location')
+ folder_layout = QtWidgets.QFormLayout(group_box_plugin)
+ self.inputs_autolab[main_key] = {}
+
+ addPluginButton = QtWidgets.QPushButton('Add driver folder')
+ addPluginButton.setIcon(icons['add'])
+ addPluginButton.clicked.connect(lambda: self.addOptionClicked(
+ folder_layout, self.inputs_autolab, 'extra_driver_path',
+ 'onedrive',
+ r'C:\Users\username\OneDrive\my_drivers'))
+ group_layout.addWidget(addPluginButton)
+
+ for sub_key, saved_value in autolab_config[main_key].items():
+ self.addOptionClicked(
+ folder_layout, self.inputs_autolab, main_key, sub_key, saved_value)
+
+ ## extra_driver_url_repo
+ main_key = 'extra_driver_url_repo'
+ group_box = QtWidgets.QFrame()
+ group_layout = QtWidgets.QVBoxLayout(group_box)
+ group_layout.setContentsMargins(0,0,0,0)
+ layoutAutolab.addWidget(group_box)
+ group_box_plugin = QtWidgets.QGroupBox(main_key)
+ group_layout.addWidget(group_box_plugin)
+ group_box.setToolTip('Add extra url to download drivers from')
+ url_layout = QtWidgets.QFormLayout(group_box_plugin)
+ self.inputs_autolab[main_key] = {}
+
+ addPluginButton = QtWidgets.QPushButton('Add driver url')
+ addPluginButton.setIcon(icons['add'])
+ addPluginButton.clicked.connect(lambda: self.addOptionClicked(
+ url_layout, self.inputs_autolab, 'extra_driver_url_repo',
+ r'C:\Users\username\OneDrive\my_drivers',
+ r'https://github.com/my_repo/my_drivers'))
+ group_layout.addWidget(addPluginButton)
+
+ for sub_key, saved_value in autolab_config[main_key].items():
+ self.addOptionClicked(
+ url_layout, self.inputs_autolab, main_key, sub_key, saved_value)
+
+ ### Plotter
+ ## plugin
+ main_key = 'plugin'
+ group_box = QtWidgets.QFrame()
+ group_layout = QtWidgets.QVBoxLayout(group_box)
+ group_layout.setContentsMargins(0,0,0,0)
+ group_layout.setSpacing(0)
+ layoutPlotter.addWidget(group_box)
+ group_box_plugin = QtWidgets.QGroupBox(main_key)
+ group_layout.addWidget(group_box_plugin)
+ group_box_plugin.setToolTip('Add plugins to plotter')
+ plugin_layout = QtWidgets.QFormLayout(group_box_plugin)
+ self.inputs_plotter[main_key] = {}
+
+ addPluginButton = QtWidgets.QPushButton('Add plugin')
+ addPluginButton.setIcon(icons['add'])
+ addPluginButton.clicked.connect(lambda: self.addOptionClicked(
+ plugin_layout, self.inputs_plotter, 'plugin', 'plugin', 'plotter'))
+ group_layout.addWidget(addPluginButton)
+
+ for sub_key, saved_value in plotter_config[main_key].items():
+ self.addOptionClicked(
+ plugin_layout, self.inputs_plotter, main_key, sub_key, saved_value)
+ # To disable plotter modification
+ input_key_widget, input_widget = self.inputs_plotter[main_key][sub_key]
+ if sub_key == 'plotter':
+ input_key_widget.setEnabled(False)
+ input_widget.setEnabled(False)
+
+ ## device
+ main_key = 'device'
+ group_box = QtWidgets.QGroupBox(main_key)
+ layoutPlotter.addWidget(group_box)
+ group_layout = QtWidgets.QFormLayout(group_box)
+ self.inputs_plotter[main_key] = {}
+
+ sub_key = 'address'
+ saved_value = plotter_config[main_key][sub_key]
+ input_widget = QtWidgets.QLineEdit()
+ input_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_widget.setToolTip('Select the address of a device variable to be captured by the plotter.')
+ input_widget.setText(str(saved_value))
+ self.inputs_plotter[main_key][sub_key] = input_widget
+ group_layout.addRow(QtWidgets.QLabel(sub_key), input_widget)
+
+ ### Buttons
+ button_box = QtWidgets.QDialogButtonBox(
+ QtWidgets.QDialogButtonBox.Ok
+ | QtWidgets.QDialogButtonBox.Cancel
+ | QtWidgets.QDialogButtonBox.Apply,
+ self)
+
+ layout.addWidget(button_box)
+ button_box.setContentsMargins(6,6,6,6)
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ apply_button = button_box.button(QtWidgets.QDialogButtonBox.Apply)
+ apply_button.clicked.connect(self.apply)
+
+ def addOptionClicked(self, main_layout: QtWidgets.QLayout,
+ main_inputs: dict, main_key: str, sub_key: str, val: str):
+ """ Add new option layout """
+ basename = sub_key
+ names = main_inputs[main_key].keys()
+ compt = 0
+ while True:
+ if sub_key in names:
+ compt += 1
+ sub_key = basename + '_' + str(compt)
+ else:
+ break
+
+ layout = QtWidgets.QHBoxLayout()
+ input_key_widget = QtWidgets.QLineEdit()
+ input_key_widget.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ input_key_widget.setText(sub_key)
+ layout.addWidget(input_key_widget)
+ input_widget = QtWidgets.QLineEdit()
+ input_widget.setText(val)
+ layout.addWidget(input_widget)
+ button_widget = QtWidgets.QPushButton()
+ button_widget.setIcon(icons['remove'])
+ button_widget.clicked.connect(lambda: self.removeOptionClicked(
+ layout, main_inputs, main_key, sub_key))
+ layout.addWidget(button_widget)
+
+ main_inputs[main_key][sub_key] = (input_key_widget, input_widget)
+ main_layout.addRow(layout)
+
+ def removeOptionClicked(self, layout: QtWidgets.QLayout,
+ main_inputs: dict, main_key: str,
+ sub_key: str):
+ """ Remove optional argument layout """
+ for j in reversed(range(layout.count())):
+ layout.itemAt(j).widget().setParent(None)
+ layout.setParent(None)
+ main_inputs[main_key].pop(sub_key)
+
+ def accept(self):
+ try:
+ self.generate_new_configs()
+ except Exception as e:
+ self.setStatus(f'Config error: {e}', 10000, False)
+ else:
+ self.close()
+
+ def apply(self):
+ try:
+ self.generate_new_configs()
+ except Exception as e:
+ self.setStatus(f'Config error: {e}', 10000, False)
+
+ def reject(self):
+ self.close()
+
+ @staticmethod
+ def generate_new_dict(input_widgets: Dict[str, QtWidgets.QWidget]) -> dict:
+
+ new_dict = {}
+ for main_key, sub_dict in input_widgets.items():
+ new_dict[main_key] = {}
+ for sub_key, widget in sub_dict.items():
+ if isinstance(widget, QtWidgets.QSpinBox):
+ new_dict[main_key][sub_key] = widget.value()
+ elif isinstance(widget, QtWidgets.QDoubleSpinBox):
+ new_dict[main_key][sub_key] = widget.value()
+ elif isinstance(widget, QtWidgets.QCheckBox):
+ new_dict[main_key][sub_key] = widget.isChecked()
+ elif isinstance(widget, QtWidgets.QComboBox):
+ new_dict[main_key][sub_key] = widget.currentText()
+ elif isinstance(widget, tuple):
+ key_widget, value_widget = widget
+ new_dict[main_key][key_widget.text()] = value_widget.text()
+ else:
+ new_dict[main_key][sub_key] = widget.text()
+
+ return new_dict
+
+ def generate_new_configs(self):
+ new_autolab_dict = self.generate_new_dict(self.inputs_autolab)
+ new_autolab_config = modify_config('autolab_config', new_autolab_dict)
+ autolab_config = load_config('autolab_config')
+
+ if new_autolab_config != autolab_config:
+ change_autolab_config(new_autolab_config)
+
+ new_plotter_dict = self.generate_new_dict(self.inputs_plotter)
+ new_plotter_config = modify_config('plotter_config', new_plotter_dict)
+ plotter_config = load_config('plotter_config')
+
+ if new_plotter_config != plotter_config:
+ change_plotter_config(new_plotter_config)
+
+ if (new_autolab_config['directories']['temp_folder']
+ != autolab_config['directories']['temp_folder']):
+ set_temp_folder()
+
+ if (new_autolab_config['control_center']['logger'] != autolab_config['control_center']['logger']
+ and hasattr(self.mainGui, 'activate_logger')):
+ self.mainGui.activate_logger(new_autolab_config['control_center']['logger'])
+
+ if (new_autolab_config['control_center']['console'] != autolab_config['control_center']['console']
+ and hasattr(self.mainGui, 'activate_console')):
+ self.mainGui.activate_console(new_autolab_config['control_center']['console'])
+
+ if (new_autolab_config['GUI']['qt_api'] != autolab_config['GUI']['qt_api']
+ or new_autolab_config['GUI']['theme'] != autolab_config['GUI']['theme']
+ or new_autolab_config['GUI']['font_size'] != autolab_config['GUI']['font_size']
+ or new_autolab_config['GUI']['image_background'] != autolab_config['GUI']['image_background']
+ or new_autolab_config['GUI']['image_background'] != autolab_config['GUI']['image_background']
+ ):
+ QtWidgets.QMessageBox.information(
+ self,
+ 'Information',
+ 'One or more of the settings you changed requires a restart to be applied',
+ QtWidgets.QMessageBox.Ok
+ )
+
+ def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
+ """ Modify the message displayed in the status bar and add error message to logger """
+ self.statusBar.showMessage(message, timeout)
+ if not stdout: print(message, file=sys.stderr)
+
+ def closeEvent(self, event):
+ """ Does some steps before the window is really killed """
+ clearPreferences()
+
+ if not self.mainGui:
+ QtWidgets.QApplication.quit() # close the app
diff --git a/autolab/core/gui/slider.py b/autolab/core/gui/GUI_slider.py
similarity index 76%
rename from autolab/core/gui/slider.py
rename to autolab/core/gui/GUI_slider.py
index 5feffe65..443297dd 100644
--- a/autolab/core/gui/slider.py
+++ b/autolab/core/gui/GUI_slider.py
@@ -4,15 +4,18 @@
@author: jonathan
"""
-from typing import Any
+from typing import Any, Union
+import sys
import numpy as np
-from qtpy import QtCore, QtWidgets, QtGui
+from qtpy import QtCore, QtWidgets
from .icons import icons
from .GUI_utilities import get_font_size, setLineEditBackground
-from .. import config
-
+from .GUI_instances import clearSlider
+from ..config import get_control_center_config
+from ..elements import Variable as Variable_og
+from ..variables import Variable
if hasattr(QtCore.Qt.LeftButton, 'value'):
LeftButton = QtCore.Qt.LeftButton.value
@@ -20,34 +23,50 @@
LeftButton = QtCore.Qt.LeftButton
+class ProxyStyle(QtWidgets.QProxyStyle):
+ """ https://stackoverflow.com/questions/67299834/pyqt-slider-not-come-to-a-specific-location-where-i-click-but-move-to-a-certain """
+ def styleHint(self, hint, opt=None, widget=None, returnData=None):
+ res = super().styleHint(hint, opt, widget, returnData)
+ if hint == QtWidgets.QStyle.SH_Slider_AbsoluteSetButtons:
+ res |= LeftButton
+ return res
+
+
class Slider(QtWidgets.QMainWindow):
- changed = QtCore.Signal()
+ changed = QtCore.Signal() # Used by scanner to update filter when slider value changes
- def __init__(self, var: Any, item: QtWidgets.QTreeWidgetItem = None):
+ def __init__(self,
+ variable: Union[Variable, Variable_og],
+ gui: QtWidgets.QMainWindow = None,
+ item: QtWidgets.QTreeWidgetItem = None):
""" https://stackoverflow.com/questions/61717896/pyqt5-qslider-is-off-by-one-depending-on-which-direction-the-slider-is-moved """
-
- self.is_main = not isinstance(item, QtWidgets.QTreeWidgetItem)
- super().__init__()
- self.variable = var
+ self.gui = gui # gui can have setStatus, threadManager
+ self.variable = variable
self.item = item
- self.main_gui = self.item.gui if hasattr(self.item, 'gui') else None
+ super().__init__()
self.resize(self.minimumSizeHint())
self.setWindowTitle(self.variable.address())
- self.setWindowIcon(QtGui.QIcon(icons['slider']))
+ self.setWindowIcon(icons['slider'])
# Load configuration
- control_center_config = config.get_control_center_config()
- self.precision = int(control_center_config['precision'])
+ control_center_config = get_control_center_config()
+ self.precision = int(float(control_center_config['precision']))
- self._font_size = get_font_size() + 1
+ self._font_size = get_font_size()
# Slider
self.slider_instantaneous = True
+ self._last_moved = False # Prevent double setting/readding after a slider has been moved with the slider_instantaneous=True
- self.true_min = self.variable.type(0)
- self.true_max = self.variable.type(10)
- self.true_step = self.variable.type(1)
+ if self.is_writable():
+ self.true_min = self.variable.type(0)
+ self.true_max = self.variable.type(10)
+ self.true_step = self.variable.type(1)
+ else:
+ self.true_min = 0
+ self.true_max = 10
+ self.true_step = 1
centralWidget = QtWidgets.QWidget()
layoutWindow = QtWidgets.QVBoxLayout()
@@ -61,7 +80,9 @@ def __init__(self, var: Any, item: QtWidgets.QTreeWidgetItem = None):
layoutWindow.addLayout(layoutBottomValues)
self.instantCheckBox = QtWidgets.QCheckBox()
- self.instantCheckBox.setToolTip("True: Changes instantaneously the value.\nFalse: Changes the value when click released.")
+ self.instantCheckBox.setToolTip(
+ "True: Changes instantaneously the value.\n" \
+ "False: Changes the value when click released.")
self.instantCheckBox.setCheckState(QtCore.Qt.Checked)
self.instantCheckBox.stateChanged.connect(self.instantChanged)
@@ -136,9 +157,25 @@ def __init__(self, var: Any, item: QtWidgets.QTreeWidgetItem = None):
self.resize(self.minimumSizeHint())
+ def displayError(self, e: str):
+ """ Wrapper to display errors """
+ self.gui.setStatus(e, 10000, False) if self.gui and hasattr(
+ self.gui, 'setStatus') else print(e, file=sys.stderr)
+
+ def setVariableValue(self, value: Any):
+ """ Wrapper to change variable value """
+ if self.gui and hasattr(self.gui, 'threadManager') and self.item:
+ self.gui.threadManager.start(
+ self.item, 'write', value=value)
+ else:
+ self.variable(value)
+
+ def is_writable(self):
+ return self.variable.writable and self.variable.type in (int, float)
+
def updateStep(self):
- if self.variable.type in (int, float):
+ if self.is_writable():
slider_points = 1 + int(
np.floor((self.true_max - self.true_min) / self.true_step))
self.true_max = self.variable.type(
@@ -162,7 +199,7 @@ def updateStep(self):
def updateTrueValue(self, old_true_value: Any):
- if self.variable.type in (int, float):
+ if self.is_writable():
new_cursor_step = round(
(old_true_value - self.true_min) / self.true_step)
slider_points = 1 + int(
@@ -185,17 +222,15 @@ def updateTrueValue(self, old_true_value: Any):
def stepWidgetValueChanged(self):
- if self.variable.type in (int, float):
+ if self.is_writable():
old_true_value = self.variable.type(self.valueWidget.text())
try:
true_step = self.variable.type(self.stepWidget.text())
assert true_step != 0, "Can't have step=0"
self.true_step = true_step
except Exception as e:
- if self.main_gui:
- self.main_gui.setStatus(
- f"Variable {self.variable.name}: {e}", 10000, False)
- # OPTIMIZE: else print ?
+ e = f"Variable {self.variable.address()}: {e}"
+ self.displayError(e)
else:
self.updateStep()
self.updateTrueValue(old_true_value)
@@ -203,14 +238,13 @@ def stepWidgetValueChanged(self):
def minWidgetValueChanged(self):
- if self.variable.type in (int, float):
+ if self.is_writable():
old_true_value = self.variable.type(self.valueWidget.text())
try:
self.true_min = self.variable.type(self.minWidget.text())
except Exception as e:
- if self.main_gui:
- self.main_gui.setStatus(
- f"Variable {self.variable.name}: {e}", 10000, False)
+ e = f"Variable {self.variable.address()}: {e}"
+ self.displayError(e)
else:
self.updateStep()
self.updateTrueValue(old_true_value)
@@ -218,14 +252,13 @@ def minWidgetValueChanged(self):
def maxWidgetValueChanged(self):
- if self.variable.type in (int, float):
+ if self.is_writable():
old_true_value = self.variable.type(self.valueWidget.text())
try:
self.true_max = self.variable.type(self.maxWidget.text())
except Exception as e:
- if self.main_gui:
- self.main_gui.setStatus(
- f"Variable {self.variable.name}: {e}", 10000, False)
+ e = f"Variable {self.variable.address()}: {e}"
+ self.displayError(e)
else:
self.updateStep()
self.updateTrueValue(old_true_value)
@@ -233,35 +266,32 @@ def maxWidgetValueChanged(self):
def sliderReleased(self):
""" Do something when the cursor is released """
- if self.variable.type in (int, float):
+ if self.is_writable():
+ if self.slider_instantaneous and self._last_moved:
+ self._last_moved = False
+ return None
+ self._last_moved = False
value = self.sliderWidget.value()
true_value = self.variable.type(
value*self.true_step + self.true_min)
self.valueWidget.setText(f'{true_value:.{self.precision}g}')
setLineEditBackground(self.valueWidget, 'synced', self._font_size)
- if self.main_gui and hasattr(self.main_gui, 'threadManager'):
- self.main_gui.threadManager.start(
- self.item, 'write', value=true_value)
- else:
- self.variable(true_value)
-
+ self.setVariableValue(true_value)
self.changed.emit()
self.updateStep()
- else: self.badType()
+ else:
+ self.badType()
def valueChanged(self, value: Any):
""" Do something with the slider value when the cursor is moved """
- if self.variable.type in (int, float):
+ if self.is_writable():
+ self._last_moved = True
true_value = self.variable.type(
value*self.true_step + self.true_min)
self.valueWidget.setText(f'{true_value:.{self.precision}g}')
if self.slider_instantaneous:
setLineEditBackground(self.valueWidget, 'synced', self._font_size)
- if self.main_gui and hasattr(self.main_gui, 'threadManager'):
- self.main_gui.threadManager.start(
- self.item, 'write', value=true_value)
- else:
- self.variable(true_value)
+ self.setVariableValue(true_value)
self.changed.emit()
else:
setLineEditBackground(self.valueWidget, 'edited', self._font_size)
@@ -273,10 +303,12 @@ def instantChanged(self, value):
def minusClicked(self):
self.sliderWidget.setSliderPosition(self.sliderWidget.value()-1)
+ self._last_moved = False
if not self.slider_instantaneous: self.sliderReleased()
def plusClicked(self):
self.sliderWidget.setSliderPosition(self.sliderWidget.value()+1)
+ self._last_moved = False
if not self.slider_instantaneous: self.sliderReleased()
def badType(self):
@@ -285,18 +317,12 @@ def badType(self):
setLineEditBackground(self.stepWidget, 'edited', self._font_size)
setLineEditBackground(self.maxWidget, 'edited', self._font_size)
+ e = f"Variable {self.variable.address()}: Variable should be writable int or float"
+ self.displayError(e)
+
def closeEvent(self, event):
""" This function does some steps before the window is really killed """
- if hasattr(self.item, 'clearSlider'): self.item.clearSlider()
+ clearSlider(self.variable)
- if self.is_main:
+ if self.gui is None:
QtWidgets.QApplication.quit() # close the slider app
-
-
-class ProxyStyle(QtWidgets.QProxyStyle):
- """ https://stackoverflow.com/questions/67299834/pyqt-slider-not-come-to-a-specific-location-where-i-click-but-move-to-a-certain """
- def styleHint(self, hint, opt=None, widget=None, returnData=None):
- res = super().styleHint(hint, opt, widget, returnData)
- if hint == QtWidgets.QStyle.SH_Slider_AbsoluteSetButtons:
- res |= LeftButton
- return res
diff --git a/autolab/core/gui/GUI_utilities.py b/autolab/core/gui/GUI_utilities.py
index 4b2360a1..3f41fb3e 100644
--- a/autolab/core/gui/GUI_utilities.py
+++ b/autolab/core/gui/GUI_utilities.py
@@ -5,16 +5,24 @@
@author: jonathan
"""
-from typing import Tuple
+import re
import os
import sys
+from typing import Tuple, List, Union, Callable
+from collections import defaultdict
+import inspect
import numpy as np
+import pandas as pd
from qtpy import QtWidgets, QtCore, QtGui
import pyqtgraph as pg
+from .icons import icons
+from ..paths import PATHS
from ..config import get_GUI_config
-
+from ..devices import DEVICES, get_element_by_address
+from ..variables import has_eval, EVAL, VARIABLES
+from ..utilities import SUPPORTED_EXTENSION
# Fixes pyqtgraph/issues/3018 for pg<=0.13.7 (before pyqtgraph/pull/3070)
from pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem
@@ -35,11 +43,10 @@ def _fourierTransform_fixed(self, x, y):
def get_font_size() -> int:
GUI_config = get_GUI_config()
- if GUI_config['font_size'] != 'default':
- font_size = int(GUI_config['font_size'])
- else:
- font_size = QtWidgets.QApplication.instance().font().pointSize()
- return font_size
+
+ return (int(float(GUI_config['font_size']))
+ if GUI_config['font_size'] != 'default'
+ else QtWidgets.QApplication.instance().font().pointSize())
def setLineEditBackground(obj, state: str, font_size: int = None):
@@ -48,13 +55,12 @@ def setLineEditBackground(obj, state: str, font_size: int = None):
if state == 'edited': color='#FFE5AE' # orange
if font_size is None:
-
obj.setStyleSheet(
- "QLineEdit:enabled {background-color: %s}" % (
+ "QLineEdit:enabled {background-color: %s; color: #000000;}" % (
color))
else:
obj.setStyleSheet(
- "QLineEdit:enabled {background-color: %s; font-size: %ipt}" % (
+ "QLineEdit:enabled {background-color: %s; font-size: %ipt; color: #000000;}" % (
color, font_size))
@@ -70,9 +76,12 @@ def qt_object_exists(QtObject) -> bool:
if not CHECK_ONCE: return True
try:
- if QT_API in ("pyqt5", "pyqt6"):
+ if QT_API == "pyqt5":
import sip
return not sip.isdeleted(QtObject)
+ if QT_API == "pyqt6":
+ from PyQt6 import sip
+ return not sip.isdeleted(QtObject)
if QT_API == "pyside2":
import shiboken2
return shiboken2.isValid(QtObject)
@@ -98,8 +107,10 @@ def __init__(self):
ax = self.addPlot()
self.ax = ax
- ax.setLabel("bottom", ' ', **{'color':0.4, 'font-size': '12pt'})
- ax.setLabel("left", ' ', **{'color':0.4, 'font-size': '12pt'})
+ ax.setLabel("bottom", ' ', **{'color': pg.getConfigOption("foreground"),
+ 'font-size': '12pt'})
+ ax.setLabel("left", ' ', **{'color': pg.getConfigOption("foreground"),
+ 'font-size': '12pt'})
# Set your custom font for both axes
my_font = QtGui.QFont('Arial', 12)
@@ -109,14 +120,14 @@ def __init__(self):
ax.getAxis("bottom").setTickFont(my_font_tick)
ax.getAxis("left").setTickFont(my_font_tick)
ax.showGrid(x=True, y=True)
- ax.setContentsMargins(10., 10., 10., 10.)
vb = ax.getViewBox()
vb.enableAutoRange(enable=True)
- vb.setBorder(pg.mkPen(color=0.4))
+ vb.setBorder(pg.mkPen(color=pg.getConfigOption("foreground")))
## Text label for the data coordinates of the mouse pointer
- dataLabel = pg.LabelItem(color='k', parent=ax.getAxis('bottom'))
+ dataLabel = pg.LabelItem(color=pg.getConfigOption("foreground"),
+ parent=ax.getAxis('bottom'))
dataLabel.anchor(itemPos=(1,1), parentPos=(1,1), offset=(0,0))
def mouseMoved(point):
@@ -205,6 +216,13 @@ def update_img(self, x, y, z):
self.img = img
self.ax.addItem(self.img)
+ def dragLeaveEvent(self, event):
+ # Pyside6 triggers a leave event resuling in error:
+ # QGraphicsView::dragLeaveEvent: drag leave received before drag enter
+ pass
+ # super().dragLeaveEvent(event)
+
+
def pyqtgraph_fig_ax() -> Tuple[MyGraphicsLayoutWidget, pg.PlotItem]:
""" Return a formated fig and ax pyqtgraph for a basic plot """
@@ -223,7 +241,7 @@ def __init__(self, *args, **kwargs):
self.figLineROI, self.axLineROI = pyqtgraph_fig_ax()
self.figLineROI.hide()
- self.plot = self.axLineROI.plot([], [], pen='k')
+ self.plot = self.axLineROI.plot([], [], pen=pg.getConfigOption("foreground"))
self.lineROI = pg.LineSegmentROI([[0, 100], [100, 100]], pen='r')
self.lineROI.sigRegionChanged.connect(self.updateLineROI)
@@ -274,6 +292,19 @@ def __init__(self, *args, **kwargs):
centralWidget.setLayout(verticalLayoutMain)
self.centralWidget = centralWidget
+ for splitter in (splitter, ):
+ for i in range(splitter.count()):
+ handle = splitter.handle(i)
+ handle.setStyleSheet("background-color: #DDDDDD;")
+ handle.installEventFilter(self)
+
+ def eventFilter(self, obj, event):
+ if event.type() == QtCore.QEvent.Enter:
+ obj.setStyleSheet("background-color: #AAAAAA;") # Hover color
+ elif event.type() == QtCore.QEvent.Leave:
+ obj.setStyleSheet("background-color: #DDDDDD;") # Normal color
+ return super().eventFilter(obj, event)
+
def update_ticks(self):
for tick in self.ui.histogram.gradient.ticks:
tick.pen = pg.mkPen(pg.getConfigOption("foreground"))
@@ -326,3 +357,533 @@ def pyqtgraph_image() -> Tuple[myImageView, QtWidgets.QWidget]:
""" Return a formated ImageView and pyqtgraph widget for image plotting """
imageView = myImageView()
return imageView, imageView.centralWidget
+
+
+class MyLineEdit(QtWidgets.QLineEdit):
+ """https://stackoverflow.com/questions/28956693/pyqt5-qtextedit-auto-completion"""
+
+ skip_has_eval = False
+ use_variables = True
+ use_devices = True
+ use_np_pd = True
+ completer: QtWidgets.QCompleter = None
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+ def create_keywords(self) -> List[str]:
+ """ Returns a list of all available keywords for completion """
+ list_keywords = []
+
+ if self.use_variables:
+ list_keywords += list(VARIABLES)
+ ## Could use this to see attributes of Variable like .raw .value
+ # list_keywords += [f'{name}.{item}'
+ # for name, var in VARIABLES.items()
+ # for item in dir(var)
+ # if not item.startswith('_') and not item.isupper()]
+ if self.use_devices:
+ list_keywords += [str(get_element_by_address(elements[0]).address())
+ for device in DEVICES.values()
+ if device.name not in list_keywords
+ for elements in device.get_structure()]
+
+ if self.use_np_pd:
+ if 'np' not in list_keywords:
+ list_keywords += ['np']
+ list_keywords += [f'np.{item}'
+ for item in dir(np)
+ if not item.startswith('_') and not item.isupper()]
+
+ if 'pd' not in list_keywords:
+ list_keywords += ['pd']
+ list_keywords += [f'pd.{item}'
+ for item in dir(pd)
+ if not item.startswith('_') and not item.isupper()]
+ return list_keywords
+
+ def eventFilter(self, obj, event):
+ """ Used when installEventFilter active """
+ if (event.type() == QtCore.QEvent.KeyPress
+ and event.key() == QtCore.Qt.Key_Tab):
+ self.keyPressEvent(event)
+ return True
+ return super().eventFilter(obj, event)
+
+ def setCompleter(self, completer: QtWidgets.QCompleter):
+ """ Sets/removes completer """
+ if self.completer:
+ if self.completer.popup().isVisible(): return None
+ self.completer.popup().close()
+ try:
+ self.completer.disconnect() # PyQT
+ except TypeError:
+ self.completer.disconnect(self) # Pyside
+
+ if not completer:
+ self.removeEventFilter(self)
+ self.completer = None
+ return None
+
+ self.installEventFilter(self)
+ completer.setWidget(self)
+ completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
+ self.completer = completer
+ self.completer.activated.connect(self.insertCompletion)
+
+ def getCompletion(self) -> List[str]:
+ model = self.completer.model()
+ return model.stringList() if isinstance(
+ model, QtCore.QStringListModel) else []
+
+ def insertCompletion(self, completion: str, prefix: bool = True):
+ cursor_pos = self.cursorPosition()
+ text = self.text()
+
+ prefix_length = (len(self.completer.completionPrefix())
+ if (prefix and self.completer) else 0)
+
+ # Replace the current word with the completion
+ new_text = (text[:cursor_pos - prefix_length]
+ + completion
+ + text[cursor_pos:])
+ self.setText(new_text)
+ self.setCursorPosition(cursor_pos - prefix_length + len(completion))
+
+ def textUnderCursor(self) -> str:
+ text = self.text()
+ cursor_pos = self.cursorPosition()
+ start = text.rfind(' ', 0, cursor_pos) + 1
+ return text[start:cursor_pos]
+
+ def focusInEvent(self, event):
+ if self.completer:
+ self.completer.setWidget(self)
+ super().focusInEvent(event)
+
+ def keyPressEvent(self, event):
+ controlPressed = event.modifiers() == QtCore.Qt.ControlModifier
+ tabPressed = event.key() == QtCore.Qt.Key_Tab
+ specialTabPressed = tabPressed and controlPressed
+ enterPressed = event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return)
+ specialEnterPressed = enterPressed and controlPressed
+
+ if specialTabPressed:
+ self.insertCompletion('\t', prefix=False)
+ return None
+
+ if specialEnterPressed:
+ self.insertCompletion('\n', prefix=False)
+ return None
+
+ # Fixe issue if press control after an eval (issue appears if do self.completer = None)
+ if self.completer and controlPressed:
+ super().keyPressEvent(event)
+ return None
+
+ if not self.completer or not tabPressed:
+ if (self.completer and enterPressed and self.completer.popup().isVisible()):
+ self.completer.activated.emit(
+ self.completer.popup().currentIndex().data())
+ else:
+ super().keyPressEvent(event)
+
+ if self.skip_has_eval or has_eval(self.text()):
+ if not self.completer:
+ self.setCompleter(QtWidgets.QCompleter(self.create_keywords()))
+ else:
+ if self.completer:
+ self.setCompleter(None)
+
+ if self.completer and not tabPressed:
+ self.completer.popup().close()
+
+ if not self.completer or not tabPressed:
+ return None
+
+ completion_prefix = self.format_completion_prefix(self.textUnderCursor())
+
+ new_keywords = self.create_new_keywords(
+ self.create_keywords(), completion_prefix)
+ keywords = self.getCompletion()
+
+ if new_keywords != keywords:
+ keywords = new_keywords
+ self.setCompleter(QtWidgets.QCompleter(keywords))
+
+ if completion_prefix != self.completer.completionPrefix():
+ self.completer.setCompletionPrefix(completion_prefix)
+ self.completer.popup().setCurrentIndex(
+ self.completer.completionModel().index(0, 0))
+
+ if self.completer.completionModel().rowCount() == 1:
+ self.completer.setCompletionMode(
+ QtWidgets.QCompleter.InlineCompletion)
+ self.completer.complete()
+
+ self.completer.activated.emit(self.completer.currentCompletion())
+ self.completer.setCompletionMode(
+ QtWidgets.QCompleter.PopupCompletion)
+ else:
+ cr = self.cursorRect()
+ cr.setWidth(self.completer.popup().sizeHintForColumn(0)
+ + self.completer.popup().verticalScrollBar().sizeHint().width())
+ self.completer.complete(cr)
+
+ @staticmethod
+ def format_completion_prefix(completion_prefix: str) -> str:
+ """ Returns a simplified prefix for completion """
+ if has_eval(completion_prefix):
+ completion_prefix = completion_prefix[len(EVAL):]
+
+ pattern = r'[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*'
+ temp = [var for var in re.findall(pattern, completion_prefix)]
+
+ if len(temp) > 0:
+ position = completion_prefix.rfind(temp[-1])
+ if position != -1:
+ completion_prefix = completion_prefix[position:]
+
+ special_char = ('(', '[', ',', ':', '-', '+', '^', '*', '/', '|')
+ if completion_prefix.endswith(special_char):
+ completion_prefix = ''
+
+ return completion_prefix
+
+ @staticmethod
+ def create_new_keywords(list_keywords: List[str],
+ completion_prefix: str) -> List[str]:
+ """ Returns a list with all available keywords and possible decomposition """
+ # Create ordered list with all attributes and sub attributes
+ master_list = []
+ master_list.append(list_keywords)
+ list_temp = list_keywords
+ while len(list_temp) > 1:
+ list_temp = list(set(
+ [var[:-len(var.split('.')[-1])-1]
+ for var in list_temp if len(var.split('.')) != 0]))
+ if '' in list_temp: list_temp.remove('')
+ master_list.append(list_temp)
+
+ # Filter attributes that contained completion_prefix and remove doublons
+ flat_list = list(set(item
+ for sublist in master_list
+ for item in sublist
+ if item.startswith(completion_prefix)))
+
+ # Group items by the number of dots
+ dot_groups = defaultdict(list)
+ for item in flat_list:
+ dot_count = item.count('.')
+ dot_groups[dot_count].append(item)
+
+ # Sort the groups by the number of dots
+ sorted_groups = sorted(
+ dot_groups.items(), key=lambda x: x[0], reverse=True)
+
+ # Extract items from each group and return as a list with sorted sublist
+ sorted_list = [sorted(group) for _, group in sorted_groups]
+
+ # Create list of all available keywords and possible decomposition
+ new_keywords = []
+ good = False
+ for level in reversed(sorted_list):
+ for item in level:
+ if completion_prefix in item:
+ new_keywords.append(item)
+ good = True
+ if good: break
+
+ return new_keywords
+
+
+class MyInputDialog(QtWidgets.QDialog):
+
+ def __init__(self, parent: QtWidgets.QMainWindow, name: str):
+
+ super().__init__(parent)
+ self.setWindowTitle(name)
+
+ lineEdit = MyLineEdit()
+ lineEdit.setMaxLength(10000000)
+ self.lineEdit = lineEdit
+
+ # Add OK and Cancel buttons
+ button_box = QtWidgets.QDialogButtonBox(
+ QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
+ self)
+
+ # Connect buttons
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addWidget(QtWidgets.QLabel(f"Set {name} value"))
+ layout.addWidget(lineEdit)
+ layout.addWidget(button_box)
+ layout.addStretch()
+ layout.setContentsMargins(10, 5, 10, 10)
+
+ self.textValue = lineEdit.text
+ self.setTextValue = lineEdit.setText
+ self.resize(self.minimumSizeHint())
+
+ def showEvent(self, event):
+ """Focus and select the text in the lineEdit."""
+ super().showEvent(event)
+ self.lineEdit.setFocus()
+ self.lineEdit.selectAll()
+
+ def closeEvent(self, event):
+ for children in self.findChildren(QtWidgets.QWidget):
+ children.deleteLater()
+ super().closeEvent(event)
+
+
+class MyFileDialog(QtWidgets.QDialog):
+
+ def __init__(self, parent: QtWidgets.QMainWindow, name: str,
+ mode: QtWidgets.QFileDialog):
+
+ super().__init__(parent)
+ if mode == QtWidgets.QFileDialog.AcceptOpen:
+ self.setWindowTitle(f"Open file - {name}")
+ elif mode == QtWidgets.QFileDialog.AcceptSave:
+ self.setWindowTitle(f"Save file - {name}")
+
+ file_dialog = QtWidgets.QFileDialog(self, QtCore.Qt.Widget)
+ file_dialog.setAcceptMode(mode)
+ file_dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog)
+ file_dialog.setWindowFlags(file_dialog.windowFlags() & ~QtCore.Qt.Dialog)
+ file_dialog.setDirectory(PATHS['last_folder'])
+ file_dialog.setNameFilters(SUPPORTED_EXTENSION.split(";;"))
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addWidget(file_dialog)
+ layout.addStretch()
+ layout.setSpacing(0)
+ layout.setContentsMargins(0,0,0,0)
+
+ self.exec_ = file_dialog.exec_
+ self.selectedFiles = file_dialog.selectedFiles
+
+ def closeEvent(self, event):
+ for children in self.findChildren(QtWidgets.QWidget):
+ children.deleteLater()
+
+ super().closeEvent(event)
+
+
+class MyQCheckBox(QtWidgets.QCheckBox):
+
+ def __init__(self, parent):
+ self.parent = parent
+ super().__init__()
+
+ def mouseReleaseEvent(self, event):
+ super().mouseReleaseEvent(event)
+ self.parent.valueEdited()
+ try:
+ inspect.signature(self.parent.write)
+ except ValueError: pass # For built-in method (occurs for boolean for action parameter)
+ else:
+ self.parent.write()
+
+
+class MyQComboBox(QtWidgets.QComboBox):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.readonly = False
+ self.wheel = True
+ self.key = True
+
+ def mousePressEvent(self, event):
+ if not self.readonly:
+ super().mousePressEvent(event)
+
+ def keyPressEvent(self, event):
+ if not self.readonly and self.key:
+ super().keyPressEvent(event)
+
+ def wheelEvent(self, event):
+ if not self.readonly and self.wheel:
+ super().wheelEvent(event)
+
+
+class CustomMenu(QtWidgets.QMenu):
+ """ Menu with action containing sub-menu for recipe and parameter selection """
+ def __init__(self,
+ gui: QtWidgets.QMainWindow,
+ main_combobox: QtWidgets.QComboBox = None,
+ second_combobox: QtWidgets.QComboBox = None,
+ update_gui: Callable[[None], None] = None):
+
+ super().__init__()
+ self.gui = gui # gui is scanner
+ self.main_combobox = main_combobox
+ self.second_combobox = second_combobox
+ self.update_gui = update_gui
+
+ self.current_menu = 1
+ self.selected_action = None
+
+ self.recipe_names = [self.main_combobox.itemText(i)
+ for i in range(self.main_combobox.count())
+ ] if self.main_combobox else []
+
+ self.HAS_RECIPE = len(self.recipe_names) > 1
+
+ self.HAS_PARAM = (self.second_combobox.count() > 1
+ if self.second_combobox
+ else False)
+
+ # To only show parameter if only one recipe
+ if not self.HAS_RECIPE and self.HAS_PARAM:
+ self.main_combobox = second_combobox
+ self.second_combobox = None
+
+ self.recipe_names = [self.main_combobox.itemText(i)
+ for i in range(self.main_combobox.count())
+ ] if self.main_combobox else []
+
+
+ def addAnyAction(self, action_text='', icon_name='',
+ param_menu_active=False) -> Union[QtWidgets.QWidgetAction,
+ QtWidgets.QAction]:
+
+ if self.HAS_RECIPE or (self.HAS_PARAM and param_menu_active):
+ action = self.addCustomAction(action_text, icon_name,
+ param_menu_active=param_menu_active)
+ else:
+ action = self.addAction(action_text)
+ if icon_name != '':
+ action.setIcon(icons[icon_name])
+
+ return action
+
+ def addCustomAction(self, action_text='', icon_name='',
+ param_menu_active=False) -> QtWidgets.QWidgetAction:
+ """ Create an action with a sub menu for selecting a recipe and parameter """
+
+ def close_menu():
+ self.selected_action = action_widget
+ self.close()
+
+ def handle_hover():
+ """ Fixe bad hover behavior and refresh radio_button """
+ self.setActiveAction(action_widget)
+ recipe_name = self.main_combobox.currentText()
+ action = recipe_menu.actions()[self.recipe_names.index(recipe_name)]
+ radio_button = action.defaultWidget()
+
+ if not radio_button.isChecked():
+ radio_button.setChecked(True)
+
+ def handle_radio_click(name):
+ """ Update parameters available, open parameter menu if available
+ and close main menu to validate the action """
+ if self.current_menu == 1:
+ self.main_combobox.setCurrentIndex(self.recipe_names.index(name))
+ if self.update_gui:
+ self.update_gui()
+ recipe_menu.close()
+
+ if param_menu_active:
+ param_items = [self.second_combobox.itemText(i)
+ for i in range(self.second_combobox.count())
+ ] if self.second_combobox else []
+
+ if len(param_items) > 1:
+ self.current_menu = 2
+ setup_menu_parameter(param_menu)
+ return None
+ else:
+ update_parameter(name)
+ param_menu.close()
+ self.current_menu = 1
+ action_button.setMenu(recipe_menu)
+
+ close_menu()
+
+ def reset_menu(button: QtWidgets.QToolButton):
+ QtWidgets.QApplication.sendEvent(
+ button, QtCore.QEvent(QtCore.QEvent.Leave))
+ self.current_menu = 1
+ action_button.setMenu(recipe_menu)
+
+ def setup_menu_parameter(param_menu: QtWidgets.QMenu):
+ param_items = [self.second_combobox.itemText(i)
+ for i in range(self.second_combobox.count())]
+ param_name = self.second_combobox.currentText()
+
+ param_menu.clear()
+ for param_name_i in param_items:
+ add_radio_button_to_menu(param_name_i, param_name, param_menu)
+
+ action_button.setMenu(param_menu)
+ action_button.showMenu()
+
+ def update_parameter(name: str):
+ param_items = [self.second_combobox.itemText(i)
+ for i in range(self.second_combobox.count())]
+ self.second_combobox.setCurrentIndex(param_items.index(name))
+ if self.update_gui:
+ self.update_gui()
+
+ def add_radio_button_to_menu(item_name: str, current_name: str,
+ target_menu: QtWidgets.QMenu):
+ widget = QtWidgets.QFrame()
+ radio_button = QtWidgets.QRadioButton(item_name, widget)
+ action = QtWidgets.QWidgetAction(self.gui)
+ action.setDefaultWidget(radio_button)
+ target_menu.addAction(action)
+
+ if item_name == current_name:
+ radio_button.setChecked(True)
+
+ radio_button.clicked.connect(
+ lambda: handle_radio_click(item_name))
+
+ # Add custom action
+ action_button = QtWidgets.QToolButton()
+ action_button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
+ action_button.setText(f" {action_text}")
+ action_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
+ action_button.setAutoRaise(True)
+ action_button.clicked.connect(close_menu)
+ action_button.enterEvent = lambda event: handle_hover()
+ if icon_name != '':
+ action_button.setIcon(icons[icon_name])
+
+ action_widget = QtWidgets.QWidgetAction(action_button)
+ action_widget.setDefaultWidget(action_button)
+ self.addAction(action_widget)
+
+ recipe_menu = QtWidgets.QMenu()
+ # recipe_menu.aboutToShow.connect(lambda: self.set_clickable(False))
+ recipe_menu.aboutToHide.connect(lambda: reset_menu(action_button))
+
+ if param_menu_active:
+ param_menu = QtWidgets.QMenu()
+ # param_menu.aboutToShow.connect(lambda: self.set_clickable(False))
+ param_menu.aboutToHide.connect(lambda: reset_menu(action_button))
+
+ recipe_name = self.main_combobox.currentText()
+
+ for recipe_name_i in self.recipe_names:
+ add_radio_button_to_menu(recipe_name_i, recipe_name, recipe_menu)
+
+ action_button.setMenu(recipe_menu)
+
+ return action_widget
+
+
+class RecipeMenu(CustomMenu):
+
+ def __init__(self, gui: QtWidgets.QMainWindow): # gui is scanner
+
+ main_combobox = gui.selectRecipe_comboBox if gui else None
+ second_combobox = gui.selectParameter_comboBox if gui else None
+ update_gui = gui._updateSelectParameter if gui else None
+
+ super().__init__(gui, main_combobox, second_combobox, update_gui)
diff --git a/autolab/core/gui/GUI_variables.py b/autolab/core/gui/GUI_variables.py
new file mode 100644
index 00000000..a6b1cb95
--- /dev/null
+++ b/autolab/core/gui/GUI_variables.py
@@ -0,0 +1,432 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Mar 4 14:54:41 2024
+
+@author: Jonathan
+"""
+
+from typing import Union
+import sys
+import re
+
+import numpy as np
+import pandas as pd
+from qtpy import QtCore, QtWidgets, QtGui
+
+from .GUI_utilities import setLineEditBackground, MyLineEdit
+from .icons import icons
+from ..devices import DEVICES
+from ..utilities import data_to_str, str_to_data, clean_string
+from ..variables import (VARIABLES, get_variable, set_variable, Variable,
+ rename_variable, remove_variable, is_Variable,
+ has_variable, has_eval, eval_variable, EVAL)
+from ..elements import Variable as Variable_og
+from ..devices import get_element_by_address
+from .GUI_instances import (openMonitor, openSlider, openPlotter,
+ closeMonitors, closeSliders, closePlotter,
+ clearVariablesMenu)
+
+
+class VariablesMenu(QtWidgets.QMainWindow):
+
+ variableSignal = QtCore.Signal(object)
+ deviceSignal = QtCore.Signal(object)
+
+ def __init__(self, has_parent: bool = False):
+
+ super().__init__()
+ self.has_parent = has_parent # Only for closeEvent
+ self.setWindowTitle('AUTOLAB - Variables Menu')
+ self.setWindowIcon(icons['variables'])
+
+ self.statusBar = self.statusBar()
+
+ # Main widgets creation
+ self.variablesWidget = QtWidgets.QTreeWidget(self)
+ self.variablesWidget.setHeaderLabels(
+ ['', 'Name', 'Value', 'Evaluated value', 'Type', 'Action'])
+ self.variablesWidget.setAlternatingRowColors(True)
+ self.variablesWidget.setIndentation(0)
+ header = self.variablesWidget.header()
+ header.setMinimumSectionSize(20)
+ header.resizeSection(0, 20)
+ header.resizeSection(1, 90)
+ header.resizeSection(2, 120)
+ header.resizeSection(3, 120)
+ header.resizeSection(4, 50)
+ header.resizeSection(5, 100)
+ self.variablesWidget.itemDoubleClicked.connect(self.variableActivated)
+ self.variablesWidget.setSelectionMode(
+ QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.variablesWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.variablesWidget.customContextMenuRequested.connect(self.rightClick)
+
+ addButton = QtWidgets.QPushButton('Add')
+ addButton.clicked.connect(self.addVariableAction)
+
+ removeButton = QtWidgets.QPushButton('Remove')
+ removeButton.clicked.connect(self.removeVariableAction)
+
+ self.devicesWidget = QtWidgets.QTreeWidget(self)
+ self.devicesWidget.setHeaderLabels(['Name'])
+ self.devicesWidget.setAlternatingRowColors(True)
+ self.devicesWidget.setIndentation(10)
+ self.devicesWidget.itemDoubleClicked.connect(self.deviceActivated)
+ self.devicesWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.devicesWidget.customContextMenuRequested.connect(self.rightClickDevice)
+
+ # Main layout creation
+ layoutWindow = QtWidgets.QVBoxLayout()
+ layoutWindow.setContentsMargins(0,0,0,0)
+ layoutWindow.setSpacing(0)
+ layoutTab = QtWidgets.QVBoxLayout()
+ layoutWindow.addLayout(layoutTab)
+
+ centralWidget = QtWidgets.QWidget()
+ centralWidget.setLayout(layoutWindow)
+ self.setCentralWidget(centralWidget)
+
+ refreshButtonWidget = QtWidgets.QPushButton()
+ refreshButtonWidget.setText('Refresh Manager')
+ refreshButtonWidget.clicked.connect(self.refresh)
+
+ # Main layout definition
+ layoutButton = QtWidgets.QHBoxLayout()
+ layoutButton.addWidget(addButton)
+ layoutButton.addWidget(removeButton)
+ layoutButton.addStretch()
+
+ frameVariables = QtWidgets.QFrame()
+ layoutVariables = QtWidgets.QVBoxLayout(frameVariables)
+ layoutVariables.addWidget(self.variablesWidget)
+ layoutVariables.addLayout(layoutButton)
+
+ frameDevices = QtWidgets.QFrame()
+ layoutDevices = QtWidgets.QVBoxLayout(frameDevices)
+ layoutDevices.addWidget(self.devicesWidget)
+
+ tab = QtWidgets.QTabWidget(self)
+ tab.addTab(frameVariables, 'Variables')
+ tab.addTab(frameDevices, 'Devices')
+
+ layoutTab.addWidget(tab)
+ layoutTab.addWidget(refreshButtonWidget)
+
+ self.resize(550, 300)
+ self.refresh()
+
+ # self.timer = QtCore.QTimer(self)
+ # self.timer.setInterval(400) # ms
+ # self.timer.timeout.connect(self.refresh_new)
+ # self.timer.start()
+ # VARIABLES.removeVarSignal.remove.connect(self.removeVarSignalChanged)
+ # VARIABLES.addVarSignal.add.connect(self.addVarSignalChanged)
+
+ def variableActivated(self, item: QtWidgets.QTreeWidgetItem):
+ self.variableSignal.emit(item.name)
+
+ def rightClick(self, position: QtCore.QPoint):
+ """ Provides a menu where the user right clicked to manage a variable """
+ item = self.variablesWidget.itemAt(position)
+ if hasattr(item, 'menu'): item.menu(position)
+
+ def rightClickDevice(self, position: QtCore.QPoint):
+ """ Provides a menu where the user right clicked to manage a variable """
+ item = self.devicesWidget.itemAt(position)
+ if hasattr(item, 'menu'): item.menu(position)
+
+ def deviceActivated(self, item: QtWidgets.QTreeWidgetItem):
+ if hasattr(item, 'name'): self.deviceSignal.emit(item.name)
+
+ def removeVariableAction(self):
+ for variableItem in self.variablesWidget.selectedItems():
+ remove_variable(variableItem.name)
+ self.removeVariableItem(variableItem)
+
+ # def addVariableItem(self, name):
+ # MyQTreeWidgetItem(self.variablesWidget, name, self)
+
+ def removeVariableItem(self, item: QtWidgets.QTreeWidgetItem):
+ index = self.variablesWidget.indexFromItem(item)
+ self.variablesWidget.takeTopLevelItem(index.row())
+
+ def addVariableAction(self):
+ basename = 'var'
+ name = basename
+ names = list(VARIABLES)
+
+ compt = 0
+ while True:
+ if name in names:
+ compt += 1
+ name = basename + str(compt)
+ else:
+ break
+
+ variable = set_variable(name, 0)
+
+ MyQTreeWidgetItem(self.variablesWidget, name, variable, self) # not catched by VARIABLES signal
+
+ # def addVarSignalChanged(self, key, value):
+ # print('got add signal', key, value)
+ # all_items = [self.variablesWidget.topLevelItem(i) for i in range(
+ # self.variablesWidget.topLevelItemCount())]
+
+ # for variableItem in all_items:
+ # if variableItem.name == key:
+ # variableItem.raw_value = get_variable(variableItem.name)
+ # variableItem.refresh_rawValue()
+ # variableItem.refresh_value()
+ # break
+ # else:
+ # self.addVariableItem(key)
+ # # self.refresh() # TODO: check if item exists, create if not, update if yes
+
+ # def removeVarSignalChanged(self, key):
+ # print('got remove signal', key)
+ # all_items = [self.variablesWidget.topLevelItem(i) for i in range(
+ # self.variablesWidget.topLevelItemCount())]
+
+ # for variableItem in all_items:
+ # if variableItem.name == key:
+ # self.removeVariableItem(variableItem)
+
+ # # self.refresh() # TODO: check if exists, remove if yes
+
+ def refresh(self):
+ self.variablesWidget.clear()
+ for var_name in VARIABLES:
+ variable = get_variable(var_name)
+ MyQTreeWidgetItem(self.variablesWidget, var_name, variable, self)
+
+ self.devicesWidget.clear()
+ for device_name, device in DEVICES.items():
+ deviceItem = QtWidgets.QTreeWidgetItem(
+ self.devicesWidget, [device_name])
+ deviceItem.setBackground(0, QtGui.QColor('#9EB7F5')) # blue
+ deviceItem.setExpanded(True)
+ for elements in device.get_structure():
+ var = get_element_by_address(elements[0])
+ MyQTreeWidgetItem(deviceItem, var.address(), var, self)
+
+ def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
+ """ Modify the message displayed in the status bar and add error message to logger """
+ self.statusBar.showMessage(message, timeout)
+ if not stdout: print(message, file=sys.stderr)
+
+ def closeEvent(self, event):
+ # self.timer.stop()
+ clearVariablesMenu()
+
+ self.variablesWidget.clear()
+ self.devicesWidget.clear()
+
+ if not self.has_parent:
+ closePlotter()
+ closeMonitors()
+ closeSliders()
+
+ import pyqtgraph as pg
+ try:
+ # Prevent 'RuntimeError: wrapped C/C++ object of type ViewBox has been deleted' when reloading gui
+ for view in pg.ViewBox.AllViews.copy().keys():
+ pg.ViewBox.forgetView(id(view), view)
+ # OPTIMIZE: forget only view used in monitor/gui
+ pg.ViewBox.quit()
+ except: pass
+
+ for children in self.findChildren(QtWidgets.QWidget):
+ children.deleteLater()
+
+ super().closeEvent(event)
+
+ if not self.has_parent:
+ QtWidgets.QApplication.quit() # close the app
+
+
+class MyQTreeWidgetItem(QtWidgets.QTreeWidgetItem):
+
+ def __init__(self,
+ itemParent: Union[QtWidgets.QTreeWidget,
+ QtWidgets.QTreeWidgetItem],
+ name: str,
+ variable: Union[Variable, Variable_og],
+ gui: QtWidgets.QMainWindow):
+
+ self.name = name
+ self.variable = variable
+ self.gui = gui
+
+ if is_Variable(self.variable):
+ super().__init__(itemParent, ['', name])
+ else:
+ super().__init__(itemParent, [name])
+ return None
+
+ nameWidget = QtWidgets.QLineEdit()
+ nameWidget.setText(name)
+ nameWidget.setAlignment(QtCore.Qt.AlignCenter)
+ nameWidget.returnPressed.connect(self.renameVariable)
+ nameWidget.textEdited.connect(lambda: setLineEditBackground(
+ nameWidget, 'edited'))
+ setLineEditBackground(nameWidget, 'synced')
+ self.gui.variablesWidget.setItemWidget(self, 1, nameWidget)
+ self.nameWidget = nameWidget
+
+ rawValueWidget = MyLineEdit()
+ rawValueWidget.setMaxLength(10000000)
+ rawValueWidget.setAlignment(QtCore.Qt.AlignCenter)
+ rawValueWidget.returnPressed.connect(self.changeRawValue)
+ rawValueWidget.textEdited.connect(lambda: setLineEditBackground(
+ rawValueWidget, 'edited'))
+ self.gui.variablesWidget.setItemWidget(self, 2, rawValueWidget)
+ self.rawValueWidget = rawValueWidget
+
+ valueWidget = QtWidgets.QLineEdit()
+ valueWidget.setMaxLength(10000000)
+ valueWidget.setReadOnly(True)
+ palette = valueWidget.palette()
+ palette.setColor(QtGui.QPalette.Base,
+ palette.color(QtGui.QPalette.Base).darker(107))
+ valueWidget.setPalette(palette)
+ valueWidget.setAlignment(QtCore.Qt.AlignCenter)
+ self.gui.variablesWidget.setItemWidget(self, 3, valueWidget)
+ self.valueWidget = valueWidget
+
+ typeWidget = QtWidgets.QLabel()
+ typeWidget.setAlignment(QtCore.Qt.AlignCenter)
+ self.gui.variablesWidget.setItemWidget(self, 4, typeWidget)
+ self.typeWidget = typeWidget
+
+ self.actionButtonWidget = None
+
+ self.refresh_rawValue()
+ self.refresh_value()
+
+ def menu(self, position: QtCore.QPoint):
+ """ This function provides the menu when the user right click on an item """
+ menu = QtWidgets.QMenu()
+ monitoringAction = menu.addAction("Start monitoring")
+ monitoringAction.setIcon(icons['monitor'])
+ monitoringAction.setEnabled(
+ (hasattr(self.variable, 'readable') # Action don't have readable
+ and self.variable.readable
+ and self.variable.type in (int, float, np.ndarray, pd.DataFrame)
+ ) or (
+ is_Variable(self.variable)
+ and (has_eval(self.variable.raw) or isinstance(
+ self.variable.value, (int, float, np.ndarray, pd.DataFrame)))
+ ))
+
+ plottingAction = menu.addAction("Capture to plotter")
+ plottingAction.setIcon(icons['plotter'])
+ plottingAction.setEnabled(monitoringAction.isEnabled())
+
+ menu.addSeparator()
+ sliderAction = menu.addAction("Create a slider")
+ sliderAction.setIcon(icons['slider'])
+ sliderAction.setEnabled(
+ (hasattr(self.variable, 'writable')
+ and self.variable.writable
+ and self.variable.type in (int, float)))
+
+ choice = menu.exec_(
+ self.gui.variablesWidget.viewport().mapToGlobal(position))
+ if choice == monitoringAction:
+ openMonitor(self.variable, has_parent=True)
+ if choice == plottingAction:
+ openPlotter(variable=self.variable, has_parent=True)
+ elif choice == sliderAction:
+ openSlider(self.variable, gui=self.gui, item=self)
+
+ def renameVariable(self) -> None:
+ new_name = self.nameWidget.text()
+ if new_name == self.name:
+ setLineEditBackground(self.nameWidget, 'synced')
+ return None
+
+ if new_name in VARIABLES:
+ self.gui.setStatus(
+ f"Error: {new_name} already exist!", 10000, False)
+ return None
+
+ new_name = clean_string(new_name)
+
+ try:
+ rename_variable(self.name, new_name)
+ except Exception as e:
+ self.gui.setStatus(f'Error: {e}', 10000, False)
+ else:
+ self.name = new_name
+ new_name = self.nameWidget.setText(self.name)
+ setLineEditBackground(self.nameWidget, 'synced')
+ self.gui.setStatus('')
+ return None
+
+ def refresh_rawValue(self):
+ raw_value_str = data_to_str(self.variable.raw)
+
+ self.rawValueWidget.setText(raw_value_str)
+ setLineEditBackground(self.rawValueWidget, 'synced')
+
+ if has_variable(self.variable.raw): # OPTIMIZE: use hide and show instead but doesn't hide on instantiation
+ if self.actionButtonWidget is None:
+ actionButtonWidget = QtWidgets.QPushButton()
+ actionButtonWidget.setText('Update value')
+ actionButtonWidget.setMinimumSize(0, 23)
+ actionButtonWidget.setMaximumSize(85, 23)
+ actionButtonWidget.clicked.connect(self.convertVariableClicked)
+ self.gui.variablesWidget.setItemWidget(self, 5, actionButtonWidget)
+ self.actionButtonWidget = actionButtonWidget
+ else:
+ self.gui.variablesWidget.removeItemWidget(self, 5)
+ self.actionButtonWidget = None
+
+ def refresh_value(self):
+ value = self.variable.value
+ value_str = data_to_str(value)
+
+ self.valueWidget.setText(value_str)
+ self.typeWidget.setText(str(type(value)).split("'")[1])
+
+
+ def changeRawValue(self):
+ name = self.name
+ raw_value = self.rawValueWidget.text()
+ try:
+ if not has_eval(raw_value):
+ raw_value = str_to_data(raw_value)
+ else:
+ # get all variables
+ raw_value_check = raw_value[len(EVAL): ] # Allows variable with name 'eval'
+ pattern1 = r'[a-zA-Z][a-zA-Z0-9._]*'
+ matches1 = re.findall(pattern1, raw_value_check)
+ # get variables not unclosed by ' or " (gives bad name so needs to check with all variables)
+ pattern2 = r'(? str:
""" Returns the name of the recipe that will receive the variables
@@ -544,58 +615,32 @@ def getParameterName(self) -> str:
return self.scanner.selectParameter_comboBox.currentText()
- def clearScanner(self):
- """ This clear the scanner instance reference when quitted """
- self.scanner = None
-
- def clearPlotter(self):
- """ This deactivate the plotter when quitted but keep the instance in memory """
- if self.plotter is not None:
- self.plotter.active = False # don't want to close plotter because want to keep data
-
- def clearAbout(self):
- """ This clear the about instance reference when quitted """
- self.about = None
-
- def clearAddDevice(self):
- """ This clear the addDevice instance reference when quitted """
- self.addDevice = None
-
def closeEvent(self, event):
""" This function does some steps before the window is really killed """
- if self.scanner is not None:
+ if self.scanner:
self.scanner.close()
- if self.plotter is not None:
- self.plotter.figureManager.fig.deleteLater()
- for children in self.plotter.findChildren(QtWidgets.QWidget):
- children.deleteLater()
-
- self.plotter.close()
-
- if self.about is not None:
- self.about.close()
-
- if self.addDevice is not None:
- self.addDevice.close()
-
- monitors = list(self.monitors.values())
- for monitor in monitors:
- monitor.close()
-
- for slider in list(self.sliders.values()):
- slider.close()
+ if self.scanner is not None and self.scanner.isVisible():
+ event.ignore()
+ return None
- devices.close() # close all devices
+ closePlotter()
+ closeAbout()
+ closeAddDevice()
+ closeMonitors()
+ closeSliders()
+ closeVariablesMenu()
+ closePreferences()
+ closeDriverInstaller()
- QtWidgets.QApplication.quit() # close the control center interface
+ if self.close_device_on_exit:
+ close() # close all devices
- if hasattr(self, 'stdout'):
- sys.stdout = self.stdout._stream
- sys.stderr = self.stderr._stream
+ self.remove_logger()
+ self.remove_console()
- if hasattr(self, '_logger_dock'): self._logger_dock.deleteLater()
- if hasattr(self, '_console_dock'): self._console_dock.deleteLater()
+ self.timerDevice.stop()
+ self.timerQueue.stop()
try:
# Prevent 'RuntimeError: wrapped C/C++ object of type ViewBox has been deleted' when reloading gui
@@ -605,502 +650,14 @@ def closeEvent(self, event):
pg.ViewBox.quit()
except: pass
- self.timerDevice.stop()
- self.timerQueue.stop()
-
for children in self.findChildren(QtWidgets.QWidget):
children.deleteLater()
super().closeEvent(event)
- VARIABLES.clear() # reset variables defined in the GUI
-
-
-class addDeviceWindow(QtWidgets.QMainWindow):
-
- def __init__(self, parent: QtWidgets.QMainWindow = None):
-
- super().__init__(parent)
- self.mainGui = parent
- self.setWindowTitle('Autolab - Add device')
- self.setWindowIcon(QtGui.QIcon(icons['autolab']))
-
- self.statusBar = self.statusBar()
-
- self._prev_name = ''
- self._prev_conn = ''
-
- try:
- import pyvisa as visa
- self.rm = visa.ResourceManager()
- except:
- self.rm = None
-
- self._font_size = get_font_size() + 1
-
- # Main layout creation
- layoutWindow = QtWidgets.QVBoxLayout()
- layoutWindow.setAlignment(QtCore.Qt.AlignTop)
-
- centralWidget = QtWidgets.QWidget()
- centralWidget.setLayout(layoutWindow)
- self.setCentralWidget(centralWidget)
-
- # Device nickname
- layoutDeviceNickname = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(layoutDeviceNickname)
-
- label = QtWidgets.QLabel('Device')
- label.setMinimumSize(60, 23)
- label.setMaximumSize(60, 23)
-
- self.deviceNickname = QtWidgets.QLineEdit()
- self.deviceNickname.setText('my_device')
-
- layoutDeviceNickname.addWidget(label)
- layoutDeviceNickname.addWidget(self.deviceNickname)
-
- # Driver name
- layoutDriverName = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(layoutDriverName)
-
- label = QtWidgets.QLabel('Driver')
- label.setMinimumSize(60, 23)
- label.setMaximumSize(60, 23)
-
- self.driversComboBox = QtWidgets.QComboBox()
- self.driversComboBox.addItems(drivers.list_drivers())
- self.driversComboBox.activated.connect(self.driverChanged)
-
- layoutDriverName.addWidget(label)
- layoutDriverName.addWidget(self.driversComboBox)
-
- # Driver connection
- layoutDriverConnection = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(layoutDriverConnection)
+ QtWidgets.QApplication.quit() # close the app
- label = QtWidgets.QLabel('Connection')
- label.setMinimumSize(60, 23)
- label.setMaximumSize(60, 23)
-
- self.connectionComboBox = QtWidgets.QComboBox()
- self.connectionComboBox.activated.connect(self.connectionChanged)
-
- layoutDriverConnection.addWidget(label)
- layoutDriverConnection.addWidget(self.connectionComboBox)
-
- # Driver arguments
- self.layoutDriverArgs = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(self.layoutDriverArgs)
-
- self.layoutDriverOtherArgs = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(self.layoutDriverOtherArgs)
-
- # layout for optional args
- self.layoutOptionalArg = QtWidgets.QVBoxLayout()
- layoutWindow.addLayout(self.layoutOptionalArg)
-
- # Add argument
- layoutButtonArg = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(layoutButtonArg)
-
- addOptionalArg = QtWidgets.QPushButton('Add argument')
- addOptionalArg.setMinimumSize(0, 23)
- addOptionalArg.setMaximumSize(100, 23)
- addOptionalArg.setIcon(QtGui.QIcon(icons['add']))
- addOptionalArg.clicked.connect(lambda state: self.addOptionalArgClicked())
-
- layoutButtonArg.addWidget(addOptionalArg)
- layoutButtonArg.setAlignment(QtCore.Qt.AlignLeft)
-
- # Add device
- layoutButton = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(layoutButton)
-
- self.addButton = QtWidgets.QPushButton('Add device')
- self.addButton.clicked.connect(self.addButtonClicked)
-
- layoutButton.addWidget(self.addButton)
-
- # update driver name combobox
- self.driverChanged()
-
- def addOptionalArgClicked(self, key: str = None, val: str = None):
- """ Add new layout for optional argument """
- layout = QtWidgets.QHBoxLayout()
- self.layoutOptionalArg.addLayout(layout)
-
- widget = QtWidgets.QLineEdit()
- widget.setText(key)
- layout.addWidget(widget)
- widget = QtWidgets.QLineEdit()
- widget.setText(val)
- layout.addWidget(widget)
- widget = QtWidgets.QPushButton()
- widget.setIcon(QtGui.QIcon(icons['remove']))
- widget.clicked.connect(lambda: self.removeOptionalArgClicked(layout))
- layout.addWidget(widget)
-
- def removeOptionalArgClicked(self, layout):
- """ Remove optional argument layout """
- for j in reversed(range(layout.count())):
- layout.itemAt(j).widget().setParent(None)
- layout.setParent(None)
-
- def addButtonClicked(self):
- """ Add the device to the config file """
- device_name = self.deviceNickname.text()
- driver_name = self.driversComboBox.currentText()
- conn = self.connectionComboBox.currentText()
-
- device_dict = {}
- device_dict['driver'] = driver_name
- device_dict['connection'] = conn
-
- for layout in (self.layoutDriverArgs, self.layoutDriverOtherArgs):
- for i in range(0, (layout.count()//2)*2, 2):
- key = layout.itemAt(i).widget().text()
- val = layout.itemAt(i+1).widget().text()
- device_dict[key] = val
-
- for i in range(self.layoutOptionalArg.count()):
- layout = self.layoutOptionalArg.itemAt(i).layout()
- key = layout.itemAt(0).widget().text()
- val = layout.itemAt(1).widget().text()
- device_dict[key] = val
-
- # Update devices config
- device_config = config.get_all_devices_configs()
- new_device = {device_name: device_dict}
- device_config.update(new_device)
- config.save_config('devices', device_config)
-
- if hasattr(self.mainGui, 'initialize'): self.mainGui.initialize()
-
- self.close()
-
- def modify(self, nickname: str, conf: dict):
- """ Modify existing driver (not optimized) """
-
- self.setWindowTitle('Autolab - Modify device')
- self.addButton.setText('Modify device')
-
- self.deviceNickname.setText(nickname)
- self.deviceNickname.setEnabled(False)
- driver_name = conf.pop('driver')
- conn = conf.pop('connection')
- index = self.driversComboBox.findText(driver_name)
- self.driversComboBox.setCurrentIndex(index)
- self.driverChanged()
-
- try:
- driver_lib = drivers.load_driver_lib(driver_name)
- except: pass
- else:
- list_conn = drivers.get_connection_names(driver_lib)
- if conn not in list_conn:
- if list_conn:
- self.setStatus(f"Connection {conn} not found, switch to {list_conn[0]}", 10000, False)
- conn = list_conn[0]
- else:
- self.setStatus(f"No connections available for driver '{driver_name}'", 10000, False)
- conn = ''
-
- index = self.connectionComboBox.findText(conn)
- self.connectionComboBox.setCurrentIndex(index)
- self.connectionChanged()
-
- # Used to remove default value
- try:
- driver_lib = drivers.load_driver_lib(driver_name)
- driver_class = drivers.get_driver_class(driver_lib)
- assert hasattr(driver_class, 'slot_config')
- except:
- slot_config = ''
- else:
- slot_config = f'{driver_class.slot_config}'
-
- # Update args
- for layout in (self.layoutDriverArgs, self.layoutDriverOtherArgs):
- for i in range(0, (layout.count()//2)*2, 2):
- key = layout.itemAt(i).widget().text()
- if key in conf:
- layout.itemAt(i+1).widget().setText(conf[key])
- conf.pop(key)
-
- # Update optional args
- for i in reversed(range(self.layoutOptionalArg.count())):
- layout = self.layoutOptionalArg.itemAt(i).layout()
- key = layout.itemAt(0).widget().text()
- val_tmp = layout.itemAt(1).widget().text()
- # Remove default value
- if ((key == 'slot1' and val_tmp == slot_config)
- or (key == 'slot1_name' and val_tmp == 'my_')):
- for j in reversed(range(layout.count())):
- layout.itemAt(j).widget().setParent(None)
- layout.setParent(None)
- elif key in conf:
- layout.itemAt(1).widget().setText(conf[key])
- conf.pop(key)
-
- # Add remaining optional args from config
- for key, val in conf.items():
- self.addOptionalArgClicked(key, val)
-
- def driverChanged(self):
- """ Update driver information """
- driver_name = self.driversComboBox.currentText()
-
- if driver_name == self._prev_name: return None
- self._prev_name = driver_name
-
- try:
- driver_lib = drivers.load_driver_lib(driver_name)
- except Exception as e:
- # If error with driver remove all layouts
- self.setStatus(f"Can't load {driver_name}: {e}", 10000, False)
-
- self.connectionComboBox.clear()
-
- for layout in (self.layoutDriverArgs, self.layoutDriverOtherArgs):
- for i in reversed(range(layout.count())):
- layout.itemAt(i).widget().setParent(None)
-
- for i in reversed(range(self.layoutOptionalArg.count())):
- layout = self.layoutOptionalArg.itemAt(i).layout()
- for j in reversed(range(layout.count())):
- layout.itemAt(j).widget().setParent(None)
- layout.setParent(None)
-
- return None
-
- self.setStatus('')
-
- # Update available connections
- connections = drivers.get_connection_names(driver_lib)
- self.connectionComboBox.clear()
- self.connectionComboBox.addItems(connections)
-
- # update selected connection information
- self._prev_conn = ''
- self.connectionChanged()
-
- # reset layoutDriverOtherArgs
- for i in reversed(range(self.layoutDriverOtherArgs.count())):
- self.layoutDriverOtherArgs.itemAt(i).widget().setParent(None)
-
- # used to skip doublon key
- conn = self.connectionComboBox.currentText()
- try:
- driver_instance = drivers.get_connection_class(driver_lib, conn)
- except:
- connection_args = {}
- else:
- connection_args = drivers.get_class_args(driver_instance)
-
- # populate layoutDriverOtherArgs
- driver_class = drivers.get_driver_class(driver_lib)
- other_args = drivers.get_class_args(driver_class)
- for key, val in other_args.items():
- if key in connection_args: continue
- widget = QtWidgets.QLabel()
- widget.setText(key)
- self.layoutDriverOtherArgs.addWidget(widget)
- widget = QtWidgets.QLineEdit()
- widget.setText(str(val))
- self.layoutDriverOtherArgs.addWidget(widget)
-
- # reset layoutOptionalArg
- for i in reversed(range(self.layoutOptionalArg.count())):
- layout = self.layoutOptionalArg.itemAt(i).layout()
- for j in reversed(range(layout.count())):
- layout.itemAt(j).widget().setParent(None)
- layout.setParent(None)
-
- # populate layoutOptionalArg
- if hasattr(driver_class, 'slot_config'):
- self.addOptionalArgClicked('slot1', f'{driver_class.slot_config}')
- self.addOptionalArgClicked('slot1_name', 'my_')
-
- def connectionChanged(self):
- """ Update connection information """
- conn = self.connectionComboBox.currentText()
-
- if conn == self._prev_conn: return None
- self._prev_conn = conn
-
- driver_name = self.driversComboBox.currentText()
- driver_lib = drivers.load_driver_lib(driver_name)
-
- connection_args = drivers.get_class_args(
- drivers.get_connection_class(driver_lib, conn))
-
- # reset layoutDriverArgs
- for i in reversed(range(self.layoutDriverArgs.count())):
- self.layoutDriverArgs.itemAt(i).widget().setParent(None)
-
- conn_widget = None
- # populate layoutDriverArgs
- for key, val in connection_args.items():
- widget = QtWidgets.QLabel()
- widget.setText(key)
- self.layoutDriverArgs.addWidget(widget)
-
- widget = QtWidgets.QLineEdit()
- widget.setText(str(val))
- self.layoutDriverArgs.addWidget(widget)
-
- if key == 'address':
- conn_widget = widget
-
- if self.rm is not None and conn == 'VISA':
- widget = QtWidgets.QComboBox()
- widget.clear()
- conn_list = ('Available connections',) + tuple(self.rm.list_resources())
- widget.addItems(conn_list)
- if conn_widget is not None:
- widget.activated.connect(
- lambda item, conn_widget=conn_widget: conn_widget.setText(
- widget.currentText()) if widget.currentText(
- ) != 'Available connections' else conn_widget.text())
- self.layoutDriverArgs.addWidget(widget)
-
- def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
- """ Modify the message displayed in the status bar and add error message to logger """
- self.statusBar.showMessage(message, timeout)
- if not stdout: print(message, file=sys.stderr)
-
- def closeEvent(self, event):
- """ Does some steps before the window is really killed """
- # Delete reference of this window in the control center
- if hasattr(self.mainGui, 'clearAddDevice'): self.mainGui.clearAddDevice()
-
- if self.mainGui is None:
- QtWidgets.QApplication.quit() # close the monitor app
-
-
-class AboutWindow(QtWidgets.QMainWindow):
-
- def __init__(self, parent: QtWidgets.QMainWindow = None):
-
- super().__init__(parent)
- self.mainGui = parent
- self.setWindowTitle('Autolab - About')
- self.setWindowIcon(QtGui.QIcon(icons['autolab']))
-
- versions = get_versions()
-
- # Main layout creation
- layoutWindow = QtWidgets.QVBoxLayout()
- layoutTab = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(layoutTab)
-
- centralWidget = QtWidgets.QWidget()
- centralWidget.setLayout(layoutWindow)
- self.setCentralWidget(centralWidget)
-
- frameOverview = QtWidgets.QFrame()
- layoutOverview = QtWidgets.QVBoxLayout(frameOverview)
-
- frameLegal = QtWidgets.QFrame()
- layoutLegal = QtWidgets.QVBoxLayout(frameLegal)
-
- tab = QtWidgets.QTabWidget(self)
- tab.addTab(frameOverview, 'Overview')
- tab.addTab(frameLegal, 'Legal')
-
- label_pic = QtWidgets.QLabel()
- label_pic.setPixmap(QtGui.QPixmap(icons['autolab']))
-
- label_autolab = QtWidgets.QLabel(f"
- Created by Quentin Chateiller, Python drivers originally from
- Quentin Chateiller and Bruno Garbin, for the C2N-CNRS
- (Center for Nanosciences and Nanotechnologies, Palaiseau, France)
- ToniQ team.
-
- Project continued by Jonathan Peltier, for the C2N-CNRS
- Minaphot team and Mathieu Jeannin, for the C2N-CNRS
- Odin team.
-
-
- Distributed under the terms of the
- GPL-3.0 licence
-
"""
- )
- label_legal.setOpenExternalLinks(True)
- label_legal.setWordWrap(True)
- layoutLegal.addWidget(label_legal)
-
- def closeEvent(self, event):
- """ Does some steps before the window is really killed """
- # Delete reference of this window in the control center
- if hasattr(self.mainGui, 'clearAbout'): self.mainGui.clearAbout()
-
- if self.mainGui is None:
- QtWidgets.QApplication.quit() # close the about app
-
-
-def get_versions() -> dict:
- """Information about Autolab versions """
-
- # Based on Spyder about.py (https://github.com/spyder-ide/spyder/blob/3ce32d6307302a93957594569176bc84d9c1612e/spyder/plugins/application/widgets/about.py#L40)
- versions = {
- 'autolab': __version__,
- 'python': platform.python_version(), # "2.7.3"
- 'bitness': 64 if sys.maxsize > 2**32 else 32,
- 'qt_api': qtpy.API_NAME, # PyQt5
- 'qt_api_ver': (qtpy.PYSIDE_VERSION if 'pyside' in qtpy.API
- else qtpy.PYQT_VERSION),
- 'system': platform.system(), # Linux, Windows, ...
- 'release': platform.release(), # XP, 10.6, 2.2.0, etc.
- 'pyqtgraph': pg.__version__,
- 'numpy': np.__version__,
- 'pandas': pd.__version__,
- }
- if sys.platform == 'darwin':
- versions.update(system='macOS', release=platform.mac_ver()[0])
-
- return versions
+ # OPTIMIZE: don't know if should erase variables on exit or not.
+ # Currently decided to only erase variables defined by scanner itself,
+ # keeping the ones defined by user.
+ # VARIABLES.clear() # reset variables defined in the GUI
diff --git a/autolab/core/gui/controlcenter/thread.py b/autolab/core/gui/controlcenter/thread.py
index 3fdffb93..956b7a97 100644
--- a/autolab/core/gui/controlcenter/thread.py
+++ b/autolab/core/gui/controlcenter/thread.py
@@ -10,9 +10,11 @@
from typing import Any
from qtpy import QtCore, QtWidgets
+
from ..GUI_utilities import qt_object_exists
-from ... import devices
-from ... import drivers
+from ...devices import get_final_device_config, list_loaded_devices, DEVICES, Device
+from ...drivers import load_driver_lib, get_driver
+from ...variables import update_allowed_dict
class ThreadManager:
@@ -29,20 +31,15 @@ def start(self, item: QtWidgets.QTreeWidgetItem, intType: str, value = None):
# GUI disabling
item.setDisabled(True)
- if hasattr(item, "execButton"):
- if qt_object_exists(item.execButton):
- item.execButton.setEnabled(False)
- if hasattr(item, "readButton"):
- if qt_object_exists(item.readButton):
- item.readButton.setEnabled(False)
- if hasattr(item, "valueWidget"):
- if qt_object_exists(item.valueWidget):
- item.valueWidget.setEnabled(False)
+ if hasattr(item, "execButton") and qt_object_exists(item.execButton):
+ item.execButton.setEnabled(False)
+ if hasattr(item, "readButton") and qt_object_exists(item.readButton):
+ item.readButton.setEnabled(False)
+ if hasattr(item, "valueWidget") and qt_object_exists(item.valueWidget):
+ item.valueWidget.setEnabled(False)
# disabling valueWidget deselect item and select next one, need to disable all items and reenable item
- list_item = self.gui.tree.selectedItems()
-
- for item_selected in list_item:
+ for item_selected in self.gui.tree.selectedItems():
item_selected.setSelected(False)
item.setSelected(True)
@@ -90,16 +87,17 @@ def threadFinished(self, tid: int, error: Exception):
item = self.threads[tid].item
if qt_object_exists(item):
item.setDisabled(False)
-
- if hasattr(item, "execButton"):
- if qt_object_exists(item.execButton):
- item.execButton.setEnabled(True)
- if hasattr(item, "readButton"):
- if qt_object_exists(item.readButton):
- item.readButton.setEnabled(True)
- if hasattr(item, "valueWidget"):
- if qt_object_exists(item.valueWidget):
- item.valueWidget.setEnabled(True)
+ item.setValueKnownState(-1 if error else True)
+
+ if hasattr(item, "execButton") and qt_object_exists(item.execButton):
+ item.execButton.setEnabled(True)
+ if hasattr(item, "readButton") and qt_object_exists(item.readButton):
+ item.readButton.setEnabled(True)
+ if hasattr(item, "valueWidget") and qt_object_exists(item.valueWidget):
+ item.valueWidget.setEnabled(True)
+ # Put back focus if item still selected (item.isSelected() doesn't work)
+ if item in self.gui.tree.selectedItems():
+ item.valueWidget.setFocus()
def delete(self, tid: int):
""" This function is called when a thread is about to be deleted.
@@ -142,24 +140,25 @@ def run(self):
elif self.intType == 'load': # OPTIMIZE: is very similar to get_device()
# Note that threadItemDict needs to be updated outside of thread to avoid timing error
device_name = self.item.name
- device_config = devices.get_final_device_config(device_name)
+ device_config = get_final_device_config(device_name)
- if device_name in devices.list_loaded_devices():
- assert device_config == devices.DEVICES[device_name].device_config, 'You cannot change the configuration of an existing Device. Close it first & retry, or remove the provided configuration.'
+ if device_name in list_loaded_devices():
+ assert device_config == DEVICES[device_name].device_config, 'You cannot change the configuration of an existing Device. Close it first & retry, or remove the provided configuration.'
else:
driver_kwargs = {k: v for k, v in device_config.items() if k not in ['driver', 'connection']}
- driver_lib = drivers.load_driver_lib(device_config['driver'])
+ driver_lib = load_driver_lib(device_config['driver'])
if hasattr(driver_lib, 'Driver') and 'gui' in [param.name for param in inspect.signature(driver_lib.Driver.__init__).parameters.values()]:
driver_kwargs['gui'] = self.item.gui
- instance = drivers.get_driver(device_config['driver'],
- device_config['connection'],
- **driver_kwargs)
- devices.DEVICES[device_name] = devices.Device(
+ instance = get_driver(
+ device_config['driver'], device_config['connection'],
+ **driver_kwargs)
+ DEVICES[device_name] = Device(
device_name, instance, device_config)
+ update_allowed_dict()
- self.item.gui.threadDeviceDict[id(self.item)] = devices.DEVICES[device_name]
+ self.item.gui.threadDeviceDict[id(self.item)] = DEVICES[device_name]
except Exception as e:
error = e
diff --git a/autolab/core/gui/controlcenter/treewidgets.py b/autolab/core/gui/controlcenter/treewidgets.py
index 0f90c271..ffcb1811 100644
--- a/autolab/core/gui/controlcenter/treewidgets.py
+++ b/autolab/core/gui/controlcenter/treewidgets.py
@@ -5,172 +5,24 @@
@author: qchat
"""
-
-import os
from typing import Any, Union
+import os
import pandas as pd
import numpy as np
from qtpy import QtCore, QtWidgets, QtGui
-from ..slider import Slider
-from ..monitoring.main import Monitor
-from .. import variables
from ..icons import icons
-from ..GUI_utilities import qt_object_exists
-from ... import paths, config
-from ...devices import close
-from ...utilities import (SUPPORTED_EXTENSION,
- str_to_array, array_to_str,
- dataframe_to_str, str_to_dataframe, create_array)
-
-
-class CustomMenu(QtWidgets.QMenu):
- """ Menu with action containing sub-menu for recipe and parameter selection """
-
- def __init__(self, gui):
- super().__init__()
- self.gui = gui
- self.current_menu = 1
- self.selected_action = None
-
- self.recipe_cb = self.gui.scanner.selectRecipe_comboBox if (
- self.gui.scanner) else None
- self.recipe_names = [self.recipe_cb.itemText(i) for i in range(
- self.recipe_cb.count())] if self.recipe_cb else []
- self.param_cb = self.gui.scanner.selectParameter_comboBox if (
- self.gui.scanner) else None
-
- self.HAS_RECIPE = len(self.recipe_names) > 1
- self.HAS_PARAM = (self.param_cb.count() > 1 if self.param_cb is not None
- else False)
-
- def addAnyAction(self, action_text='', icon_name='',
- param_menu_active=False) -> Union[QtWidgets.QWidgetAction,
- QtWidgets.QAction]:
-
- if self.HAS_RECIPE or (self.HAS_PARAM and param_menu_active):
- action = self.addCustomAction(action_text, icon_name,
- param_menu_active=param_menu_active)
- else:
- action = self.addAction(action_text)
- if icon_name != '':
- action.setIcon(QtGui.QIcon(icons[icon_name]))
-
- return action
-
- def addCustomAction(self, action_text='', icon_name='',
- param_menu_active=False) -> QtWidgets.QWidgetAction:
- """ Create an action with a sub menu for selecting a recipe and parameter """
-
- def close_menu():
- self.selected_action = action_widget
- self.close()
-
- def handle_hover():
- """ Fixe bad hover behavior and refresh radio_button """
- self.setActiveAction(action_widget)
- recipe_name = self.recipe_cb.currentText()
- action = recipe_menu.actions()[self.recipe_names.index(recipe_name)]
- radio_button = action.defaultWidget()
-
- if not radio_button.isChecked():
- radio_button.setChecked(True)
-
- def handle_radio_click(name):
- """ Update parameters available, open parameter menu if available
- and close main menu to validate the action """
- if self.current_menu == 1:
- self.recipe_cb.setCurrentIndex(self.recipe_names.index(name))
- self.gui.scanner._updateSelectParameter()
- recipe_menu.close()
-
- if param_menu_active:
- param_items = [self.param_cb.itemText(i) for i in range(
- self.param_cb.count())]
-
- if len(param_items) > 1:
- self.current_menu = 2
- setup_menu_parameter(param_menu)
- return None
- else:
- update_parameter(name)
- param_menu.close()
- self.current_menu = 1
- action_button.setMenu(recipe_menu)
-
- close_menu()
-
- def reset_menu(button: QtWidgets.QToolButton):
- QtWidgets.QApplication.sendEvent(
- button, QtCore.QEvent(QtCore.QEvent.Leave))
- self.current_menu = 1
- action_button.setMenu(recipe_menu)
-
- def setup_menu_parameter(param_menu: QtWidgets.QMenu):
- param_items = [self.param_cb.itemText(i) for i in range(
- self.param_cb.count())]
- param_name = self.param_cb.currentText()
-
- param_menu.clear()
- for param_name_i in param_items:
- add_radio_button_to_menu(param_name_i, param_name, param_menu)
-
- action_button.setMenu(param_menu)
- action_button.showMenu()
-
- def update_parameter(name: str):
- param_items = [self.param_cb.itemText(i) for i in range(
- self.param_cb.count())]
- self.param_cb.setCurrentIndex(param_items.index(name))
- self.gui.scanner._updateSelectParameter()
-
- def add_radio_button_to_menu(item_name: str, current_name: str,
- target_menu: QtWidgets.QMenu):
- widget = QtWidgets.QWidget()
- radio_button = QtWidgets.QRadioButton(item_name, widget)
- action = QtWidgets.QWidgetAction(self.gui)
- action.setDefaultWidget(radio_button)
- target_menu.addAction(action)
-
- if item_name == current_name:
- radio_button.setChecked(True)
-
- radio_button.clicked.connect(
- lambda: handle_radio_click(item_name))
-
- # Add custom action
- action_button = QtWidgets.QToolButton()
- action_button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
- action_button.setText(f" {action_text}")
- action_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
- action_button.setAutoRaise(True)
- action_button.clicked.connect(close_menu)
- action_button.enterEvent = lambda event: handle_hover()
- if icon_name != '':
- action_button.setIcon(QtGui.QIcon(icons[icon_name]))
-
- action_widget = QtWidgets.QWidgetAction(action_button)
- action_widget.setDefaultWidget(action_button)
- self.addAction(action_widget)
-
- recipe_menu = QtWidgets.QMenu()
- # recipe_menu.aboutToShow.connect(lambda: self.set_clickable(False))
- recipe_menu.aboutToHide.connect(lambda: reset_menu(action_button))
-
- if param_menu_active:
- param_menu = QtWidgets.QMenu()
- # param_menu.aboutToShow.connect(lambda: self.set_clickable(False))
- param_menu.aboutToHide.connect(lambda: reset_menu(action_button))
-
- recipe_name = self.gui.scanner.selectRecipe_comboBox.currentText()
-
- for recipe_name_i in self.recipe_names:
- add_radio_button_to_menu(recipe_name_i, recipe_name, recipe_menu)
-
- action_button.setMenu(recipe_menu)
-
- return action_widget
+from ..GUI_utilities import (MyLineEdit, MyInputDialog, MyQCheckBox, MyQComboBox,
+ RecipeMenu, qt_object_exists)
+from ..GUI_instances import openMonitor, openSlider, openPlotter, openAddDevice
+from ...paths import PATHS
+from ...config import get_control_center_config
+from ...variables import eval_variable, has_eval
+from ...devices import close, list_loaded_devices
+from ...utilities import (SUPPORTED_EXTENSION, str_to_array, array_to_str,
+ dataframe_to_str, str_to_dataframe, create_array,
+ str_to_tuple)
class TreeWidgetItemModule(QtWidgets.QTreeWidgetItem):
@@ -186,6 +38,9 @@ def __init__(self, itemParent, name, gui):
if self.is_not_submodule:
super().__init__(itemParent, [name, 'Device'])
+ self.indicator = QtWidgets.QLabel()
+ self.indicator.setToolTip('Device not instantiated yet')
+ self.gui.tree.setItemWidget(self, 4, self.indicator)
else:
super().__init__(itemParent, [name, 'Module'])
@@ -226,7 +81,7 @@ def menu(self, position: QtCore.QPoint):
if self.loaded:
menu = QtWidgets.QMenu()
disconnectDevice = menu.addAction(f"Disconnect {self.name}")
- disconnectDevice.setIcon(QtGui.QIcon(icons['disconnect']))
+ disconnectDevice.setIcon(icons['disconnect'])
choice = menu.exec_(self.gui.tree.viewport().mapToGlobal(position))
@@ -237,10 +92,15 @@ def menu(self, position: QtCore.QPoint):
self.removeChild(self.child(0))
self.loaded = False
+ self.setValueKnownState(False)
+
+ if not list_loaded_devices():
+ self.gui._stop_timerQueue = True
+
elif id(self) in self.gui.threadManager.threads_conn:
menu = QtWidgets.QMenu()
cancelDevice = menu.addAction('Cancel loading')
- cancelDevice.setIcon(QtGui.QIcon(icons['disconnect']))
+ cancelDevice.setIcon(icons['disconnect'])
choice = menu.exec_(self.gui.tree.viewport().mapToGlobal(position))
@@ -249,12 +109,27 @@ def menu(self, position: QtCore.QPoint):
else:
menu = QtWidgets.QMenu()
modifyDeviceChoice = menu.addAction('Modify device')
- modifyDeviceChoice.setIcon(QtGui.QIcon(icons['rename']))
+ modifyDeviceChoice.setIcon(icons['rename'])
choice = menu.exec_(self.gui.tree.viewport().mapToGlobal(position))
if choice == modifyDeviceChoice:
- self.gui.openAddDevice(self)
+ openAddDevice(gui=self.gui, name=self.name)
+
+ def setValueKnownState(self, state: Union[bool, float]):
+ """ Turn the color of the indicator depending of the known state of the value """
+ if state == 0.5:
+ self.indicator.setStyleSheet("background-color:#FFFF00") # yellow
+ self.indicator.setToolTip('Device is being instantiated')
+ elif state == -1:
+ self.indicator.setStyleSheet("background-color:#FF0000") # red
+ self.indicator.setToolTip('Device connection error')
+ elif state:
+ self.indicator.setStyleSheet("background-color:#70db70") # green
+ self.indicator.setToolTip('Device instantiated')
+ else:
+ self.indicator.setStyleSheet("background-color:none") # none
+ self.indicator.setToolTip('Device closed')
class TreeWidgetItemAction(QtWidgets.QTreeWidgetItem):
@@ -272,8 +147,12 @@ def __init__(self, itemParent, action, gui):
self.gui = gui
self.action = action
+ # Import Autolab config
+ control_center_config = get_control_center_config()
+ self.precision = int(float(control_center_config['precision']))
+
if self.action.has_parameter:
- if self.action.type in [int, float, str, np.ndarray, pd.DataFrame]:
+ if self.action.type in [int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]:
self.executable = True
self.has_value = True
else:
@@ -292,91 +171,224 @@ def __init__(self, itemParent, action, gui):
# Main - Column 3 : QlineEdit if the action has a parameter
if self.has_value:
- self.valueWidget = QtWidgets.QLineEdit()
- self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
- self.gui.tree.setItemWidget(self, 3, self.valueWidget)
- self.valueWidget.returnPressed.connect(self.execute)
+ if self.action.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
+ self.valueWidget = MyLineEdit()
+ self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
+ self.gui.tree.setItemWidget(self, 3, self.valueWidget)
+ self.valueWidget.returnPressed.connect(self.execute)
+ self.valueWidget.textEdited.connect(self.valueEdited)
+
+ ## QCheckbox for boolean variables
+ elif self.action.type in [bool]:
+ self.valueWidget = MyQCheckBox(self)
+ hbox = QtWidgets.QHBoxLayout()
+ hbox.addWidget(self.valueWidget)
+ hbox.setAlignment(QtCore.Qt.AlignCenter)
+ hbox.setSpacing(0)
+ hbox.setContentsMargins(0,0,0,0)
+ widget = QtWidgets.QFrame()
+ widget.setLayout(hbox)
+
+ self.gui.tree.setItemWidget(self, 3, widget)
+
+ ## Combobox for tuples: Tuple[List[str], int]
+ elif self.action.type in [tuple]:
+ self.valueWidget = MyQComboBox()
+ self.valueWidget.wheel = False # prevent changing value by mistake
+ self.valueWidget.key = False
+ self.valueWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.valueWidget.customContextMenuRequested.connect(self.openInputDialog)
+
+ self.gui.tree.setItemWidget(self, 3, self.valueWidget)
+
+ # Main - column 4 : indicator (status of the actual value : known or not known)
+ self.indicator = QtWidgets.QLabel()
+ self.gui.tree.setItemWidget(self, 4, self.indicator)
# Tooltip
if self.action._help is None: tooltip = 'No help available for this action'
else: tooltip = self.action._help
+ if hasattr(self.action, "type") and self.action.type is not None:
+ action_type = str(self.action.type).split("'")[1]
+ tooltip += f" ({action_type})"
self.setToolTip(0, tooltip)
+ self.writeSignal = WriteSignal()
+ self.writeSignal.writed.connect(self.valueWrited)
+ self.action._write_signal = self.writeSignal
+
+ def openInputDialog(self, position: QtCore.QPoint):
+ """ Only used for tuple """
+ menu = QtWidgets.QMenu()
+ modifyTuple = menu.addAction("Modify tuple")
+ modifyTuple.setIcon(icons['tuple'])
+
+ choice = menu.exec_(self.valueWidget.mapToGlobal(position))
+
+ if choice == modifyTuple:
+ main_dialog = MyInputDialog(self.gui, self.action.address())
+ main_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ if self.action.type in [tuple]:
+ main_dialog.setTextValue(str(self.action.value))
+ main_dialog.show()
+
+ if main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
+ response = main_dialog.textValue()
+ else:
+ response = ''
+
+ if qt_object_exists(main_dialog): main_dialog.deleteLater()
+
+ if response != '':
+ try:
+ if has_eval(response):
+ response = eval_variable(response)
+ if self.action.type in [tuple]:
+ response = str_to_tuple(str(response))
+ except Exception as e:
+ self.gui.setStatus(
+ f"Variable {self.action.address()}: {e}", 10000, False)
+ return None
+
+ self.action.value = response
+ self.valueWrited(response)
+ self.valueEdited()
+
+ def writeGui(self, value):
+ """ This function displays a new value in the GUI """
+ if hasattr(self, 'valueWidget') and qt_object_exists(self.valueWidget): # avoid crash if device closed and try to write gui (if close device before reading finished)
+ # Update value
+ if self.action.type in [int, float]:
+ self.valueWidget.setText(f'{value:.{self.precision}g}') # default is .6g
+ elif self.action.type in [str]:
+ self.valueWidget.setText(value)
+ elif self.action.type in [bytes]:
+ self.valueWidget.setText(value.decode())
+ elif self.action.type in [bool]:
+ self.valueWidget.setChecked(value)
+ elif self.action.type in [tuple]:
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
+ if value[0] != items:
+ self.valueWidget.clear()
+ self.valueWidget.addItems(value[0])
+ self.valueWidget.setCurrentIndex(value[1])
+ elif self.action.type in [np.ndarray]:
+ self.valueWidget.setText(array_to_str(value))
+ elif self.action.type in [pd.DataFrame]:
+ self.valueWidget.setText(dataframe_to_str(value))
+ else:
+ self.valueWidget.setText(value)
+
def readGui(self) -> Any:
""" This function returns the value in good format of the value in the GUI """
- value = self.valueWidget.text()
-
- if value == '':
- if self.action.unit in ('open-file', 'save-file', 'filename'):
- if self.action.unit == "filename": # TODO: LEGACY (to remove later)
- self.gui.setStatus("Using 'filename' as unit is depreciated in favor of 'open-file' and 'save-file'" \
- f"\nUpdate driver {self.action.name} to remove this warning",
- 10000, False)
- self.action.unit = "open-file"
-
- if self.action.unit == "open-file":
- filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.gui, caption=f"Open file - {self.action.name}",
- directory=paths.USER_LAST_CUSTOM_FOLDER,
- filter=SUPPORTED_EXTENSION)
- elif self.action.unit == "save-file":
- filename, _ = QtWidgets.QFileDialog.getSaveFileName(
- self.gui, caption=f"Save file - {self.action.name}",
- directory=paths.USER_LAST_CUSTOM_FOLDER,
- filter=SUPPORTED_EXTENSION)
-
- if filename != '':
- path = os.path.dirname(filename)
- paths.USER_LAST_CUSTOM_FOLDER = path
- return filename
- else:
- self.gui.setStatus(
- f"Action {self.action.name} cancel filename selection",
- 10000)
- elif self.action.unit == "user-input":
- response, _ = QtWidgets.QInputDialog.getText(
- self.gui, self.action.name, f"Set {self.action.name} value",
- QtWidgets.QLineEdit.Normal)
-
- if response != '':
- return response
+ if self.action.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
+ value = self.valueWidget.text()
+
+ if value == '':
+ if self.action.unit in ('open-file', 'save-file', 'filename'):
+ if self.action.unit == "filename": # TODO: LEGACY (to remove later)
+ self.gui.setStatus("Using 'filename' as unit is depreciated in favor of 'open-file' and 'save-file'" \
+ f"\nUpdate driver '{self.action.address().split('.')[0]}' to remove this warning",
+ 10000, False)
+ self.action.unit = "open-file"
+
+ if self.action.unit == "open-file":
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.gui, caption=f"Open file - {self.action.address()}",
+ directory=PATHS['last_folder'],
+ filter=SUPPORTED_EXTENSION)
+ elif self.action.unit == "save-file":
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
+ self.gui, caption=f"Save file - {self.action.address()}",
+ directory=PATHS['last_folder'],
+ filter=SUPPORTED_EXTENSION)
+
+ if filename != '':
+ path = os.path.dirname(filename)
+ PATHS['last_folder'] = path
+ return filename
+ else:
+ self.gui.setStatus(
+ f"Action {self.action.address()} cancel filename selection",
+ 10000)
+ elif self.action.unit == "user-input":
+ main_dialog = MyInputDialog(self.gui, self.action.address())
+ main_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ main_dialog.show()
+
+ if main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
+ response = main_dialog.textValue()
+ else:
+ response = ''
+
+ if qt_object_exists(main_dialog): main_dialog.deleteLater()
+
+ if response != '':
+ return response
+ else:
+ self.gui.setStatus(
+ f"Action {self.action.address()} cancel user input",
+ 10000)
else:
self.gui.setStatus(
- f"Action {self.action.name} cancel user input",
- 10000)
+ f"Action {self.action.address()} requires a value for its parameter",
+ 10000, False)
else:
- self.gui.setStatus(
- f"Action {self.action.name} requires a value for its parameter",
- 10000, False)
+ try:
+ value = eval_variable(value)
+ if self.action.type in [int]:
+ value = int(float(value))
+ if self.action.type in [bytes]:
+ value = value.encode()
+ elif self.action.type in [np.ndarray]:
+ value = str_to_array(value) if isinstance(
+ value, str) else create_array(value)
+ elif self.action.type in [pd.DataFrame]:
+ if isinstance(value, str):
+ value = str_to_dataframe(value)
+ else:
+ value = self.action.type(value)
+ return value
+ except Exception as e:
+ self.gui.setStatus(
+ f"Action {self.action.address()}: {e}",
+ 10000, False)
+ elif self.action.type in [bool]:
+ value = self.valueWidget.isChecked()
+ return value
+ elif self.action.type in [tuple]:
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
+ value = (items, self.valueWidget.currentIndex())
+ return value
+
+ def setValueKnownState(self, state: Union[bool, float]):
+ """ Turn the color of the indicator depending of the known state of the value """
+ if state == 0.5:
+ self.indicator.setStyleSheet("background-color:#FFFF00") # yellow
+ self.indicator.setToolTip('Value written but not read')
+ elif state:
+ self.indicator.setStyleSheet("background-color:#70db70") # green
+ self.indicator.setToolTip('Value read')
else:
- try:
- value = variables.eval_variable(value)
- if self.action.type in [np.ndarray]:
- if isinstance(value, str): value = str_to_array(value)
- elif self.action.type in [pd.DataFrame]:
- if isinstance(value, str): value = str_to_dataframe(value)
- else:
- value = self.action.type(value)
- return value
- except:
- self.gui.setStatus(
- f"Action {self.action.name}: Impossible to convert {value} to {self.action.type.__name__}",
- 10000, False)
+ self.indicator.setStyleSheet("background-color:#ff8c1a") # orange
+ self.indicator.setToolTip('Value not up-to-date')
def execute(self):
""" Start a new thread to execute the associated action """
if not self.isDisabled():
if self.has_value:
value = self.readGui()
- if value is not None: self.gui.threadManager.start(
- self, 'execute', value=value)
+ if value is not None:
+ self.gui.threadManager.start(self, 'execute', value=value)
else:
self.gui.threadManager.start(self, 'execute')
def menu(self, position: QtCore.QPoint):
""" This function provides the menu when the user right click on an item """
if not self.isDisabled():
- menu = CustomMenu(self.gui)
+ menu = RecipeMenu(self.gui.scanner)
scanRecipe = menu.addAnyAction('Do in scan recipe', 'action')
@@ -385,7 +397,22 @@ def menu(self, position: QtCore.QPoint):
if choice == scanRecipe:
recipe_name = self.gui.getRecipeName()
- self.gui.addStepToScanRecipe(recipe_name, 'action', self.action)
+ value = self.action.value if self.action.type in [tuple] else None
+ self.gui.addStepToScanRecipe(
+ recipe_name, 'action', self.action, value=value)
+
+ def valueEdited(self):
+ """ Change indicator state when editing action parameter """
+ self.setValueKnownState(False)
+
+ def valueWrited(self, value: Any):
+ """ Called when action parameter written """
+ try:
+ if self.has_value:
+ self.writeGui(value)
+ self.setValueKnownState(0.5)
+ except Exception as e:
+ self.gui.setStatus(f"SHOULD NOT RAISE ERROR: {e}", 10000, False)
class TreeWidgetItemVariable(QtWidgets.QTreeWidgetItem):
@@ -404,20 +431,20 @@ def __init__(self, itemParent, variable, gui):
self.variable = variable
# Import Autolab config
- control_center_config = config.get_control_center_config()
- self.precision = int(control_center_config['precision'])
+ control_center_config = get_control_center_config()
+ self.precision = int(float(control_center_config['precision']))
# Signal creation and associations in autolab devices instances
self.readSignal = ReadSignal()
self.readSignal.read.connect(self.writeGui)
self.variable._read_signal = self.readSignal
self.writeSignal = WriteSignal()
- self.writeSignal.writed.connect(self.valueEdited)
+ self.writeSignal.writed.connect(self.valueWrited)
self.variable._write_signal = self.writeSignal
# Main - Column 2 : Creation of a READ button if the variable is readable
if self.variable.readable and self.variable.type in [
- int, float, bool, str, tuple, np.ndarray, pd.DataFrame]:
+ int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]:
self.readButton = QtWidgets.QPushButton()
self.readButton.setText("Read")
self.readButton.clicked.connect(self.read)
@@ -425,8 +452,11 @@ def __init__(self, itemParent, variable, gui):
if not self.variable.writable and self.variable.type in [
np.ndarray, pd.DataFrame]:
self.readButtonCheck = QtWidgets.QCheckBox()
- self.readButtonCheck.stateChanged.connect(self.readButtonCheckEdited)
- self.readButtonCheck.setToolTip('Toggle reading in text, careful can truncate data and impact performance')
+ self.readButtonCheck.stateChanged.connect(
+ self.readButtonCheckEdited)
+ self.readButtonCheck.setToolTip(
+ 'Toggle reading in text, ' \
+ 'careful can truncate data and impact performance')
self.readButtonCheck.setMaximumWidth(15)
frameReadButton = QtWidgets.QFrame()
@@ -442,9 +472,9 @@ def __init__(self, itemParent, variable, gui):
# Main - column 3 : Creation of a VALUE widget, depending on the type
## QLineEdit or QLabel
- if self.variable.type in [int, float, str, np.ndarray, pd.DataFrame]:
+ if self.variable.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
if self.variable.writable:
- self.valueWidget = QtWidgets.QLineEdit()
+ self.valueWidget = MyLineEdit()
self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
self.valueWidget.returnPressed.connect(self.write)
self.valueWidget.textEdited.connect(self.valueEdited)
@@ -454,8 +484,10 @@ def __init__(self, itemParent, variable, gui):
self.valueWidget = QtWidgets.QLineEdit()
self.valueWidget.setMaxLength(10000000)
self.valueWidget.setReadOnly(True)
- self.valueWidget.setStyleSheet(
- "QLineEdit {border: 1px solid #a4a4a4; background-color: #f4f4f4}")
+ palette = self.valueWidget.palette()
+ palette.setColor(QtGui.QPalette.Base,
+ palette.color(QtGui.QPalette.Base).darker(107))
+ self.valueWidget.setPalette(palette)
self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
else:
self.valueWidget = QtWidgets.QLabel()
@@ -465,28 +497,13 @@ def __init__(self, itemParent, variable, gui):
## QCheckbox for boolean variables
elif self.variable.type in [bool]:
-
- class MyQCheckBox(QtWidgets.QCheckBox):
-
- def __init__(self, parent):
- self.parent = parent
- super().__init__()
-
- def mouseReleaseEvent(self, event):
- super().mouseReleaseEvent(event)
- self.parent.valueEdited()
- self.parent.write()
-
self.valueWidget = MyQCheckBox(self)
- # self.valueWidget = QtWidgets.QCheckBox()
- # self.valueWidget.stateChanged.connect(self.valueEdited)
- # self.valueWidget.stateChanged.connect(self.write) # removed this to avoid setting a second time when reading a change
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(self.valueWidget)
hbox.setAlignment(QtCore.Qt.AlignCenter)
hbox.setSpacing(0)
hbox.setContentsMargins(0,0,0,0)
- widget = QtWidgets.QWidget()
+ widget = QtWidgets.QFrame()
widget.setLayout(hbox)
if not self.variable.writable: # Disable interaction is not writable
self.valueWidget.setEnabled(False)
@@ -495,31 +512,13 @@ def mouseReleaseEvent(self, event):
## Combobox for tuples: Tuple[List[str], int]
elif self.variable.type in [tuple]:
-
- class MyQComboBox(QtWidgets.QComboBox):
- def __init__(self):
- super().__init__()
- self.readonly = False
- self.wheel = True
- self.key = True
-
- def mousePressEvent(self, event):
- if not self.readonly:
- super().mousePressEvent(event)
-
- def keyPressEvent(self, event):
- if not self.readonly and self.key:
- super().keyPressEvent(event)
-
- def wheelEvent(self, event):
- if not self.readonly and self.wheel:
- super().wheelEvent(event)
-
if self.variable.writable:
self.valueWidget = MyQComboBox()
self.valueWidget.wheel = False # prevent changing value by mistake
self.valueWidget.key = False
self.valueWidget.activated.connect(self.write)
+ self.valueWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.valueWidget.customContextMenuRequested.connect(self.openInputDialog)
elif self.variable.readable:
self.valueWidget = MyQComboBox()
self.valueWidget.readonly = True
@@ -530,9 +529,8 @@ def wheelEvent(self, event):
self.gui.tree.setItemWidget(self, 3, self.valueWidget)
# Main - column 4 : indicator (status of the actual value : known or not known)
- if self.variable.type in [int, float, str, bool, tuple, np.ndarray, pd.DataFrame]:
- self.indicator = QtWidgets.QLabel()
- self.gui.tree.setItemWidget(self, 4, self.indicator)
+ self.indicator = QtWidgets.QLabel()
+ self.gui.tree.setItemWidget(self, 4, self.indicator)
# Tooltip
if self.variable._help is None: tooltip = 'No help available for this variable'
@@ -545,24 +543,64 @@ def wheelEvent(self, event):
# disable read button if array/dataframe
if hasattr(self, 'readButtonCheck'): self.readButtonCheckEdited()
+ def openInputDialog(self, position: QtCore.QPoint):
+ """ Only used for tuple """
+ menu = QtWidgets.QMenu()
+ modifyTuple = menu.addAction("Modify tuple")
+ modifyTuple.setIcon(icons['tuple'])
+
+ choice = menu.exec_(self.valueWidget.mapToGlobal(position))
+
+ if choice == modifyTuple:
+ main_dialog = MyInputDialog(self.gui, self.variable.address())
+ main_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ if self.variable.type in [tuple]:
+ main_dialog.setTextValue(str(self.variable.value))
+ main_dialog.show()
+
+ if main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
+ response = main_dialog.textValue()
+ else:
+ response = ''
+
+ if qt_object_exists(main_dialog): main_dialog.deleteLater()
+
+ if response != '':
+ try:
+ if has_eval(response):
+ response = eval_variable(response)
+ if self.variable.type in [tuple]:
+ response = str_to_tuple(str(response))
+ except Exception as e:
+ self.gui.setStatus(
+ f"Variable {self.variable.address()}: {e}", 10000, False)
+ return None
+
+ self.variable(response)
+
+ if self.variable.readable:
+ self.variable()
+
def writeGui(self, value):
""" This function displays a new value in the GUI """
- if qt_object_exists(self.valueWidget): # avoid crash if device closed and try to write gui (if close device before reading finished)
+ if hasattr(self, 'valueWidget') and qt_object_exists(self.valueWidget): # avoid crash if device closed and try to write gui (if close device before reading finished)
# Update value
if self.variable.numerical:
self.valueWidget.setText(f'{value:.{self.precision}g}') # default is .6g
elif self.variable.type in [str]:
self.valueWidget.setText(value)
+ elif self.variable.type in [bytes]:
+ self.valueWidget.setText(value.decode())
elif self.variable.type in [bool]:
self.valueWidget.setChecked(value)
elif self.variable.type in [tuple]:
- items = [self.valueWidget.itemText(i) for i in range(self.valueWidget.count())]
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
if value[0] != items:
self.valueWidget.clear()
self.valueWidget.addItems(value[0])
self.valueWidget.setCurrentIndex(value[1])
elif self.variable.type in [np.ndarray, pd.DataFrame]:
-
if self.variable.writable or self.readButtonCheck.isChecked():
if self.variable.type in [np.ndarray]:
self.valueWidget.setText(array_to_str(value))
@@ -572,21 +610,26 @@ def writeGui(self, value):
# self.valueWidget.setText('')
# Change indicator light to green
- if self.variable.type in [int, float, bool, str, tuple, np.ndarray, pd.DataFrame]:
+ if self.variable.type in [
+ int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]:
self.setValueKnownState(True)
def readGui(self):
""" This function returns the value in good format of the value in the GUI """
- if self.variable.type in [int, float, str, np.ndarray, pd.DataFrame]:
+ if self.variable.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
value = self.valueWidget.text()
if value == '':
self.gui.setStatus(
- f"Variable {self.variable.name} requires a value to be set",
+ f"Variable {self.variable.address()} requires a value to be set",
10000, False)
else:
try:
- value = variables.eval_variable(value)
- if self.variable.type in [np.ndarray]:
+ value = eval_variable(value)
+ if self.variable.type in [int]:
+ value = int(float(value))
+ if self.variable.type in [bytes]:
+ value = value.encode()
+ elif self.variable.type in [np.ndarray]:
if isinstance(value, str): value = str_to_array(value)
else: value = create_array(value)
elif self.variable.type in [pd.DataFrame]:
@@ -594,23 +637,31 @@ def readGui(self):
else:
value = self.variable.type(value)
return value
- except:
+ except Exception as e:
self.gui.setStatus(
- f"Variable {self.variable.name}: Impossible to convert {value} to {self.variable.type.__name__}",
+ f"Variable {self.variable.address()}: {e}",
10000, False)
elif self.variable.type in [bool]:
value = self.valueWidget.isChecked()
return value
elif self.variable.type in [tuple]:
- items = [self.valueWidget.itemText(i) for i in range(self.valueWidget.count())]
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
value = (items, self.valueWidget.currentIndex())
return value
- def setValueKnownState(self, state: bool):
+ def setValueKnownState(self, state: Union[bool, float]):
""" Turn the color of the indicator depending of the known state of the value """
- if state: self.indicator.setStyleSheet("background-color:#70db70") # green
- else: self.indicator.setStyleSheet("background-color:#ff8c1a") # orange
+ if state == 0.5:
+ self.indicator.setStyleSheet("background-color:#FFFF00") # yellow
+ self.indicator.setToolTip('Value written but not read')
+ elif state:
+ self.indicator.setStyleSheet("background-color:#70db70") # green
+ self.indicator.setToolTip('Value read')
+ else:
+ self.indicator.setStyleSheet("background-color:#ff8c1a") # orange
+ self.indicator.setToolTip('Value not up-to-date')
def read(self):
""" Start a new thread to READ the associated variable """
@@ -625,30 +676,39 @@ def write(self):
if value is not None:
self.gui.threadManager.start(self, 'write', value=value)
+ def valueWrited(self, value: Any):
+ """ Function call when the value displayed in not sure anymore.
+ The value has been modified either in the GUI (but not sent)
+ or by command line.
+ If variable not readable, write the value sent to the GUI """
+ # BUG: I got an error when changing emit_write to set value, need to reproduce it
+ try:
+ self.writeGui(value)
+ self.setValueKnownState(0.5)
+ except Exception as e:
+ self.gui.setStatus(f"SHOULD NOT RAISE ERROR: {e}", 10000, False)
+
def valueEdited(self):
""" Function call when the value displayed in not sure anymore.
- The value has been modified either in the GUI (but not sent) or by command line """
+ The value has been modified either in the GUI (but not sent)
+ or by command line """
self.setValueKnownState(False)
def readButtonCheckEdited(self):
state = bool(self.readButtonCheck.isChecked())
-
self.readButton.setEnabled(state)
- # if not self.variable.writable:
- # self.valueWidget.setVisible(state) # doesn't work on instantiation, but not problem if start with visible
-
- # if not state: self.valueWidget.setText('')
-
def menu(self, position: QtCore.QPoint):
""" This function provides the menu when the user right click on an item """
if not self.isDisabled():
- menu = CustomMenu(self.gui)
+ menu = RecipeMenu(self.gui.scanner)
monitoringAction = menu.addAction("Start monitoring")
- monitoringAction.setIcon(QtGui.QIcon(icons['monitor']))
+ monitoringAction.setIcon(icons['monitor'])
+ plottingAction = menu.addAction("Capture to plotter")
+ plottingAction.setIcon(icons['plotter'])
menu.addSeparator()
sliderAction = menu.addAction("Create a slider")
- sliderAction.setIcon(QtGui.QIcon(icons['slider']))
+ sliderAction.setIcon(icons['slider'])
menu.addSeparator()
scanParameterAction = menu.addAnyAction(
@@ -660,26 +720,28 @@ def menu(self, position: QtCore.QPoint):
menu.addSeparator()
saveAction = menu.addAction("Read and save as...")
- saveAction.setIcon(QtGui.QIcon(icons['read-save']))
+ saveAction.setIcon(icons['read-save'])
monitoringAction.setEnabled(
self.variable.readable and self.variable.type in [
int, float, np.ndarray, pd.DataFrame])
+ plottingAction.setEnabled(monitoringAction.isEnabled())
sliderAction.setEnabled((self.variable.writable
- #and self.variable.readable
and self.variable.type in [int, float]))
scanParameterAction.setEnabled(self.variable.parameter_allowed)
scanMeasureStepAction.setEnabled(self.variable.readable)
saveAction.setEnabled(self.variable.readable)
- scanSetStepAction.setEnabled(
- self.variable.writable if self.variable.type not in [
- tuple] else False) # OPTIMIZE: forbid setting tuple to scanner
+ scanSetStepAction.setEnabled(self.variable.writable)
choice = menu.exec_(self.gui.tree.viewport().mapToGlobal(position))
if choice is None: choice = menu.selected_action
- if choice == monitoringAction: self.openMonitor()
- elif choice == sliderAction: self.openSlider()
+ if choice == monitoringAction:
+ openMonitor(self.variable, has_parent=True)
+ if choice == plottingAction:
+ openPlotter(variable=self.variable, has_parent=True)
+ elif choice == sliderAction:
+ openSlider(self.variable, gui=self.gui, item=self)
elif choice == scanParameterAction:
recipe_name = self.gui.getRecipeName()
param_name = self.gui.getParameterName()
@@ -689,64 +751,30 @@ def menu(self, position: QtCore.QPoint):
self.gui.addStepToScanRecipe(recipe_name, 'measure', self.variable)
elif choice == scanSetStepAction:
recipe_name = self.gui.getRecipeName()
- self.gui.addStepToScanRecipe(recipe_name, 'set', self.variable)
+ value = self.variable.value if self.variable.type in [tuple] else None
+ self.gui.addStepToScanRecipe(
+ recipe_name, 'set', self.variable, value=value)
elif choice == saveAction: self.saveValue()
def saveValue(self):
""" Prompt user for filename to save data of the variable """
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
- self.gui, f"Save {self.variable.name} value", os.path.join(
- paths.USER_LAST_CUSTOM_FOLDER, f'{self.variable.address()}.txt'),
+ self.gui, f"Save {self.variable.address()} value", os.path.join(
+ PATHS['last_folder'], f'{self.variable.address()}.txt'),
filter=SUPPORTED_EXTENSION)
path = os.path.dirname(filename)
if path != '':
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
try:
- self.gui.setStatus(f"Saving value of {self.variable.name}...",
- 5000)
+ self.gui.setStatus(
+ f"Saving value of {self.variable.address()}...", 5000)
self.variable.save(filename)
self.gui.setStatus(
- f"Value of {self.variable.name} successfully read and save at {filename}",
- 5000)
+ f"Value of {self.variable.address()} successfully read " \
+ f"and save at {filename}", 5000)
except Exception as e:
- self.gui.setStatus(f"An error occured: {str(e)}", 10000, False)
-
- def openMonitor(self):
- """ This function open the monitor associated to this variable. """
- # If the monitor is not already running, create one
- if id(self) not in self.gui.monitors.keys():
- self.gui.monitors[id(self)] = Monitor(self)
- self.gui.monitors[id(self)].show()
- # If the monitor is already running, just make as the front window
- else:
- monitor = self.gui.monitors[id(self)]
- monitor.setWindowState(
- monitor.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
- monitor.activateWindow()
-
- def openSlider(self):
- """ This function open the slider associated to this variable. """
- # If the slider is not already running, create one
- if id(self) not in self.gui.sliders.keys():
- self.gui.sliders[id(self)] = Slider(self.variable, self)
- self.gui.sliders[id(self)].show()
- # If the slider is already running, just make as the front window
- else:
- slider = self.gui.sliders[id(self)]
- slider.setWindowState(
- slider.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
- slider.activateWindow()
-
- def clearMonitor(self):
- """ This clear monitor instances reference when quitted """
- if id(self) in self.gui.monitors.keys():
- self.gui.monitors.pop(id(self))
-
- def clearSlider(self):
- """ This clear the slider instances reference when quitted """
- if id(self) in self.gui.sliders.keys():
- self.gui.sliders.pop(id(self))
+ self.gui.setStatus(f"An error occured: {e}", 10000, False)
# Signals can be emitted only from QObjects
@@ -757,6 +785,6 @@ def emit_read(self, value):
self.read.emit(value)
class WriteSignal(QtCore.QObject):
- writed = QtCore.Signal()
- def emit_write(self):
- self.writed.emit()
+ writed = QtCore.Signal(object)
+ def emit_write(self, value):
+ self.writed.emit(value)
diff --git a/autolab/core/gui/icons/__init__.py b/autolab/core/gui/icons/__init__.py
index 74881ed0..8313d8f0 100644
--- a/autolab/core/gui/icons/__init__.py
+++ b/autolab/core/gui/icons/__init__.py
@@ -4,87 +4,72 @@
@author: Jonathan
"""
+from typing import Dict, Union
import os
-icons_folder = os.path.dirname(__file__)
+from qtpy import QtWidgets, QtGui
+ICONS_FOLDER = os.path.dirname(__file__)
+standardIcon = QtWidgets.QApplication.style().standardIcon
-ACTION_ICON_NAME = os.path.join(icons_folder, 'action-icon.svg').replace("\\", "/")
-ADD_ICON_NAME = os.path.join(icons_folder, 'add-icon.svg').replace("\\", "/")
-AUTOLAB_ICON_NAME = os.path.join(icons_folder, 'autolab-icon.ico').replace("\\", "/")
-CONFIG_ICON_NAME = os.path.join(icons_folder, 'config-icon.svg').replace("\\", "/")
-DISCONNECT_ICON_NAME = os.path.join(icons_folder, 'disconnect-icon.svg').replace("\\", "/")
-DOWN_ICON_NAME = os.path.join(icons_folder, 'down-icon.svg').replace("\\", "/")
-EXPORT_ICON_NAME = os.path.join(icons_folder, 'export-icon.svg').replace("\\", "/")
-GITHUB_ICON_NAME = os.path.join(icons_folder, 'github-icon.svg').replace("\\", "/")
-IMPORT_ICON_NAME = os.path.join(icons_folder, 'import-icon.svg').replace("\\", "/")
-IS_DISABLE_ICON_NAME = os.path.join(icons_folder, 'is-disable-icon.svg').replace("\\", "/")
-IS_ENABLE_ICON_NAME = os.path.join(icons_folder, 'is-enable-icon.svg').replace("\\", "/")
-MEASURE_ICON_NAME = os.path.join(icons_folder, 'measure-icon.svg').replace("\\", "/")
-MONITOR_ICON_NAME = os.path.join(icons_folder, 'monitor-icon.svg').replace("\\", "/")
-PARAMETER_ICON_NAME = os.path.join(icons_folder, 'parameter-icon.svg').replace("\\", "/")
-PDF_ICON_NAME = os.path.join(icons_folder, 'pdf-icon.svg').replace("\\", "/")
-PLOTTER_ICON_NAME = os.path.join(icons_folder, 'plotter-icon.svg').replace("\\", "/")
-READ_SAVE_ICON_NAME = os.path.join(icons_folder, 'read-save-icon.svg').replace("\\", "/")
-READTHEDOCS_ICON_NAME = os.path.join(icons_folder, 'readthedocs-icon.svg').replace("\\", "/")
-RECIPE_ICON_NAME = os.path.join(icons_folder, 'recipe-icon.svg').replace("\\", "/")
-REDO_ICON_NAME = os.path.join(icons_folder, 'redo-icon.svg').replace("\\", "/")
-REMOVE_ICON_NAME = os.path.join(icons_folder, 'remove-icon.svg').replace("\\", "/")
-RENAME_ICON_NAME = os.path.join(icons_folder, 'rename-icon.svg').replace("\\", "/")
-SCANNER_ICON_NAME = os.path.join(icons_folder, 'scanner-icon.svg').replace("\\", "/")
-SLIDER_ICON_NAME = os.path.join(icons_folder, 'slider-icon.svg').replace("\\", "/")
-UNDO_ICON_NAME = os.path.join(icons_folder, 'undo-icon.svg').replace("\\", "/")
-UP_ICON_NAME = os.path.join(icons_folder, 'up-icon.svg').replace("\\", "/")
-WRITE_ICON_NAME = os.path.join(icons_folder, 'write-icon.svg').replace("\\", "/")
+def format_icon_path(name: str) -> str:
+ return os.path.join(ICONS_FOLDER, name).replace("\\", "/")
-icons = {'action': ACTION_ICON_NAME,
- 'add': ADD_ICON_NAME,
- 'autolab': AUTOLAB_ICON_NAME,
- 'config': CONFIG_ICON_NAME,
- 'disconnect': DISCONNECT_ICON_NAME,
- 'down': DOWN_ICON_NAME,
- 'export': EXPORT_ICON_NAME,
- 'github': GITHUB_ICON_NAME,
- 'import': IMPORT_ICON_NAME,
- 'is-disable': IS_DISABLE_ICON_NAME,
- 'is-enable': IS_ENABLE_ICON_NAME,
- 'measure': MEASURE_ICON_NAME,
- 'monitor': MONITOR_ICON_NAME,
- 'parameter': PARAMETER_ICON_NAME,
- 'plotter': PLOTTER_ICON_NAME,
- 'pdf': PDF_ICON_NAME,
- 'read-save': READ_SAVE_ICON_NAME,
- 'readthedocs': READTHEDOCS_ICON_NAME,
- 'recipe': RECIPE_ICON_NAME,
- 'redo': REDO_ICON_NAME,
- 'remove': REMOVE_ICON_NAME,
- 'rename': RENAME_ICON_NAME,
- 'scanner': SCANNER_ICON_NAME,
- 'slider': SLIDER_ICON_NAME,
- 'undo': UNDO_ICON_NAME,
- 'up': UP_ICON_NAME,
- 'write': WRITE_ICON_NAME,
- }
+def create_icon(name: str) -> QtGui.QIcon:
+ return QtGui.QIcon(format_icon_path(name))
-INT_ICON_NAME = os.path.join(icons_folder, 'int-icon.svg').replace("\\", "/")
-FLOAT_ICON_NAME = os.path.join(icons_folder, 'float-icon.svg').replace("\\", "/")
-STR_ICON_NAME = os.path.join(icons_folder, 'str-icon.svg').replace("\\", "/")
-BYTES_ICON_NAME = os.path.join(icons_folder, 'bytes-icon.svg').replace("\\", "/")
-BOOL_ICON_NAME = os.path.join(icons_folder, 'bool-icon.svg').replace("\\", "/")
-TUPLE_ICON_NAME = os.path.join(icons_folder, 'tuple-icon.svg').replace("\\", "/")
-NDARRAY_ICON_NAME = os.path.join(icons_folder, 'ndarray-icon.svg').replace("\\", "/")
-DATAFRAME_ICON_NAME = os.path.join(icons_folder, 'dataframe-icon.svg').replace("\\", "/")
+def create_pixmap(name: str) -> QtGui.QPixmap:
+ return QtGui.QPixmap(format_icon_path(name))
+
+
+icons: Dict[str, Union[QtGui.QIcon, QtGui.QPixmap, standardIcon]] = {
+ 'action': create_icon('action-icon.svg'),
+ 'add': create_icon('add-icon.svg'),
+ 'autolab': create_icon('autolab-icon.ico'),
+ 'autolab-pixmap': create_pixmap('autolab-icon.ico'),
+ 'config': create_icon('config-icon.svg'),
+ 'copy': create_icon('copy-icon.svg'),
+ 'disconnect': create_icon('disconnect-icon.svg'),
+ 'down': create_icon('down-icon.svg'),
+ 'export': create_icon('export-icon.svg'),
+ 'file': standardIcon(QtWidgets.QStyle.SP_FileDialogDetailedView),
+ 'folder': standardIcon(QtWidgets.QStyle.SP_DirIcon),
+ 'github': create_icon('github-icon.svg'),
+ 'import': create_icon('import-icon.svg'),
+ 'is-disable': create_icon('is-enable-icon.svg'),
+ 'is-enable': create_icon('is-enable-icon.svg'),
+ 'measure': create_icon('measure-icon.svg'),
+ 'monitor': create_icon('monitor-icon.svg'),
+ 'parameter': create_icon('parameter-icon.svg'),
+ 'paste': create_icon('paste-icon.svg'),
+ 'pdf': create_icon('pdf-icon.svg'),
+ 'plotter': create_icon('plotter-icon.svg'),
+ 'preference': create_icon('preference-icon.svg'),
+ 'read-save': create_icon('read-save-icon.svg'),
+ 'readthedocs': create_icon('readthedocs-icon.svg'),
+ # 'recipe': create_icon('recipe-icon.svg'),
+ 'redo': create_icon('redo-icon.svg'),
+ 'reload': standardIcon(QtWidgets.QStyle.SP_BrowserReload),
+ 'remove': create_icon('remove-icon.svg'),
+ 'rename': create_icon('rename-icon.svg'),
+ 'scanner': create_icon('scanner-icon.svg'),
+ 'slider': create_icon('slider-icon.svg'),
+ 'undo': create_icon('undo-icon.svg'),
+ 'up': create_icon('up-icon.svg'),
+ 'variables': create_icon('variables-icon.svg'),
+ 'write': create_icon('write-icon.svg'),
+}
icons.update({
-'int': INT_ICON_NAME,
-'float': FLOAT_ICON_NAME,
-'str': STR_ICON_NAME,
-'bytes': BYTES_ICON_NAME,
-'bool': BOOL_ICON_NAME,
-'tuple': TUPLE_ICON_NAME,
-'ndarray': NDARRAY_ICON_NAME,
-'DataFrame': DATAFRAME_ICON_NAME,
+ 'int': create_icon('int-icon.svg'),
+ 'float': create_icon('float-icon.svg'),
+ 'str': create_icon('str-icon.svg'),
+ 'bytes': create_icon('bytes-icon.svg'),
+ 'bool': create_icon('bool-icon.svg'),
+ 'tuple': create_icon('tuple-icon.svg'),
+ 'ndarray': create_icon('ndarray-icon.svg'),
+ 'DataFrame': create_icon('dataframe-icon.svg'),
})
diff --git a/autolab/core/gui/icons/copy-icon.svg b/autolab/core/gui/icons/copy-icon.svg
new file mode 100644
index 00000000..e5df5475
--- /dev/null
+++ b/autolab/core/gui/icons/copy-icon.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/autolab/core/gui/icons/github-icon.svg b/autolab/core/gui/icons/github-icon.svg
index 37fa923d..e30c6f83 100644
--- a/autolab/core/gui/icons/github-icon.svg
+++ b/autolab/core/gui/icons/github-icon.svg
@@ -1 +1,45 @@
-
\ No newline at end of file
+
+
diff --git a/autolab/core/gui/icons/measure-icon.svg b/autolab/core/gui/icons/measure-icon.svg
index ab7ee5c7..4a181b23 100644
--- a/autolab/core/gui/icons/measure-icon.svg
+++ b/autolab/core/gui/icons/measure-icon.svg
@@ -32,6 +32,10 @@
inkscape:current-layer="Layer_1" />
+ Wondicon - UI (Free)
+ style="stroke-width:0.444;stroke:#ffffff;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:nodetypes="sssss" />
+ style="fill:#ffffff;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 13.239079,7.2955734 v 7.4552886 h 7.455289 V 7.2955734 Z"
+ id="path10" />
+ style="stroke-width:0.444;stroke:#ffffff;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:nodetypes="sssss" />
+ style="fill:#ffffff;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 13.239079,31.846695 v 7.455288 h 7.455288 v -7.455288 z"
+ id="path12" />
-
+ style="stroke-width:0.444;stroke:#ffffff;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:nodetypes="ccccc" />
+
+ style="stroke-width:0.444;stroke:#ffffff;stroke-opacity:1;stroke-dasharray:none" />
+ style="stroke-width:0.444;stroke:#ffffff;stroke-opacity:1;stroke-dasharray:none" />
+ style="stroke-width:0.444;stroke:#ffffff;stroke-opacity:1;stroke-dasharray:none" />
diff --git a/autolab/core/gui/icons/recipe-icon.svg b/autolab/core/gui/icons/paste-icon.svg
similarity index 91%
rename from autolab/core/gui/icons/recipe-icon.svg
rename to autolab/core/gui/icons/paste-icon.svg
index 856268e2..a41ada2e 100644
--- a/autolab/core/gui/icons/recipe-icon.svg
+++ b/autolab/core/gui/icons/paste-icon.svg
@@ -3,7 +3,7 @@
version="1.2"
viewBox="0 0 48 48"
id="svg50"
- sodipodi:docname="recipe-icon.svg"
+ sodipodi:docname="paste-icon.svg"
width="48"
height="48"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
@@ -21,7 +21,7 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="11.313709"
- inkscape:cx="39.332813"
+ inkscape:cx="39.377007"
inkscape:cy="36.062444"
inkscape:window-width="1419"
inkscape:window-height="1038"
@@ -31,6 +31,13 @@
inkscape:current-layer="svg50" />
+
+
+
+
diff --git a/autolab/core/gui/icons/readthedocs-icon.svg b/autolab/core/gui/icons/readthedocs-icon.svg
index 304e27b2..fc31119c 100644
--- a/autolab/core/gui/icons/readthedocs-icon.svg
+++ b/autolab/core/gui/icons/readthedocs-icon.svg
@@ -1 +1,41 @@
-
\ No newline at end of file
+
+
diff --git a/autolab/core/gui/icons/remove-icon.svg b/autolab/core/gui/icons/remove-icon.svg
index 57525e04..4a478838 100644
--- a/autolab/core/gui/icons/remove-icon.svg
+++ b/autolab/core/gui/icons/remove-icon.svg
@@ -20,9 +20,9 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
- inkscape:zoom="5.7692308"
- inkscape:cx="21.493333"
- inkscape:cy="35.013333"
+ inkscape:zoom="8.1589244"
+ inkscape:cx="18.200928"
+ inkscape:cy="33.215162"
inkscape:window-width="1419"
inkscape:window-height="1038"
inkscape:window-x="485"
@@ -31,6 +31,11 @@
inkscape:current-layer="svg50" />
+
+
+
diff --git a/autolab/core/gui/icons/write-icon.svg b/autolab/core/gui/icons/write-icon.svg
index 4e51fb5d..e5712e57 100644
--- a/autolab/core/gui/icons/write-icon.svg
+++ b/autolab/core/gui/icons/write-icon.svg
@@ -22,18 +22,27 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
- inkscape:zoom="8.5520833"
- inkscape:cx="5.729598"
- inkscape:cy="12.336175"
+ inkscape:zoom="6.0472361"
+ inkscape:cx="35.388068"
+ inkscape:cy="59.614011"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
+
+
+ style="stroke-width:2.33333"
+ sodipodi:nodetypes="ccccccscccsscssccscccsssccsssssss" />
diff --git a/autolab/core/gui/monitoring/data.py b/autolab/core/gui/monitoring/data.py
index 01eec36f..cecfcf0b 100644
--- a/autolab/core/gui/monitoring/data.py
+++ b/autolab/core/gui/monitoring/data.py
@@ -48,7 +48,8 @@ def addPoint(self, point: Tuple[Any, Any]):
if isinstance(y, (np.ndarray, pd.DataFrame)):
if self.gui.windowLength_lineEdit.isVisible():
- self.gui.figureManager.setLabel('x', 'x')
+ self.gui.xlabel = 'x'
+ self.gui.figureManager.setLabel('x', self.gui.xlabel)
self.gui.windowLength_lineEdit.hide()
self.gui.windowLength_label.hide()
self.gui.dataDisplay.hide()
@@ -62,7 +63,8 @@ def addPoint(self, point: Tuple[Any, Any]):
self._addArray(y.values.T)
else:
if not self.gui.windowLength_lineEdit.isVisible():
- self.gui.figureManager.setLabel('x', 'Time [s]')
+ self.gui.xlabel = 'Time(s)'
+ self.gui.figureManager.setLabel('x', self.gui.xlabel)
self.gui.windowLength_lineEdit.show()
self.gui.windowLength_label.show()
self.gui.dataDisplay.show()
diff --git a/autolab/core/gui/monitoring/figure.py b/autolab/core/gui/monitoring/figure.py
index 487b334e..b66d413b 100644
--- a/autolab/core/gui/monitoring/figure.py
+++ b/autolab/core/gui/monitoring/figure.py
@@ -9,11 +9,11 @@
import numpy as np
import pyqtgraph as pg
-import pyqtgraph.exporters
+import pyqtgraph.exporters # Needed for pg.exporters.ImageExporter
from qtpy import QtWidgets
from ..GUI_utilities import pyqtgraph_fig_ax, pyqtgraph_image
-from ... import config
+from ...config import get_monitor_config
from ...utilities import boolean
@@ -24,7 +24,7 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self.gui = gui
# Import Autolab config
- monitor_config = config.get_monitor_config()
+ monitor_config = get_monitor_config()
self.precision = int(monitor_config['precision'])
self.do_save_figure = boolean(monitor_config['save_figure'])
@@ -41,9 +41,9 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self.plot = self.ax.plot([], [], symbol='x', pen='r', symbolPen='r',
symbolSize=10, symbolBrush='r')
self.plot_mean = self.ax.plot([], [], pen=pg.mkPen(
- color=0.4, width=2, style=pg.QtCore.Qt.DashLine))
- self.plot_min = self.ax.plot([], [], pen=pg.mkPen(color=0.4, width=2))
- self.plot_max = self.ax.plot([], [], pen=pg.mkPen(color=0.4, width=2))
+ color=pg.getConfigOption("foreground"), width=2, style=pg.QtCore.Qt.DashLine))
+ self.plot_min = self.ax.plot([], [], pen=pg.mkPen(color=pg.getConfigOption("foreground"), width=2))
+ self.plot_max = self.ax.plot([], [], pen=pg.mkPen(color=pg.getConfigOption("foreground"), width=2))
self.ymin = None
self.ymax = None
@@ -125,7 +125,8 @@ def setLabel(self, axe: str, value: str):
""" This function changes the label of the given axis """
axes = {'x':'bottom', 'y':'left'}
if value == '': value = ' '
- self.ax.setLabel(axes[axe], value, **{'color':0.4, 'font-size': '12pt'})
+ self.ax.setLabel(axes[axe], value, **{'color': pg.getConfigOption("foreground"),
+ 'font-size': '12pt'})
def clear(self):
self.ymin = None
diff --git a/autolab/core/gui/monitoring/interface.ui b/autolab/core/gui/monitoring/interface.ui
index f011e5ea..2bb65f5c 100644
--- a/autolab/core/gui/monitoring/interface.ui
+++ b/autolab/core/gui/monitoring/interface.ui
@@ -14,237 +14,253 @@
MainWindow
-
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
-
-
-
-
-
-
-
- Qt::Horizontal
+
+
+ Qt::Vertical
+
+
+
+
+
+
+
+ 0
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
+
+
+
+ Qt::Horizontal
+
+
- 100
- 16777215
+ 40
+ 20
-
-
- 9
-
-
-
- Pause delay between each measure
-
-
+
-
-
-
-
- 9
-
-
-
- Delay [s] :
+
+
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Pause delay between each measure
+
+
+
+
+
+
+ Delay [s] :
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
+ Window length [s] :
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Size of the current window in seconds
+
+
+
+
+
+
+
+
+ Qt::Horizontal
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 20
+ 20
+
-
+
-
-
-
-
- 9
-
+
+
+
+
+ 0
+ 0
+
-
- Window length [s] :
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 70
+ 20
+
-
-
-
-
- 100
- 16777215
+ 200
+ 100
-
-
- 9
-
+
+ ArrowCursor
-
- Size of the current window in seconds
+
+ false
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 20
- 20
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 70
- 20
-
-
-
-
- 200
- 100
-
-
-
- ArrowCursor
-
-
- false
-
-
- Qt::NoFocus
-
-
- false
-
-
- Qt::AlignCenter
-
-
- true
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 20
- 20
-
-
-
-
-
-
-
-
-
-
- 9
-
-
-
- Pause (or resume) monitoring
-
-
- Pause
+
+ Qt::NoFocus
-
-
-
-
-
- Mean
+
+ false
-
-
-
-
-
- Clear
+
+ Qt::AlignCenter
-
-
-
-
-
- Min
+
+ true
-
-
-
-
- 9
-
+
+
+
+ Qt::Horizontal
-
- Save data displayed
-
-
- Save
+
+
+ 20
+ 20
+
-
+
+
+
+
+
+
+
+ Save data displayed
+
+
+ Save
+
+
+
+
+
+
+ Mean
+
+
+
+
+
+
+ Max
+
+
+
+
+
+
+ Clear
+
+
+
+
+
+
+ Pause (or resume) monitoring
+
+
+ Pause
+
+
+
+
+
+
+ Min
+
+
+
+
+
+
+
+
+ Pause monitoring on scan start if contains this variable
+
+
+ Pause on scan start
+
+
+
+
+
+
+ Start on scan end
+
+
+
+
+
+
-
-
-
- Max
+
+
+
+ Qt::Horizontal
-
+
+
+ 40
+ 20
+
+
+
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
+
+
diff --git a/autolab/core/gui/monitoring/main.py b/autolab/core/gui/monitoring/main.py
index 77d19ccf..4f1862ae 100644
--- a/autolab/core/gui/monitoring/main.py
+++ b/autolab/core/gui/monitoring/main.py
@@ -4,36 +4,40 @@
@author: qchat
"""
+from typing import Union
import os
import sys
import queue
-from qtpy import QtCore, QtWidgets, uic, QtGui
+from qtpy import QtCore, QtWidgets, uic
from .data import DataManager
from .figure import FigureManager
from .monitor import MonitorManager
from ..icons import icons
-from ... import paths
from ..GUI_utilities import get_font_size, setLineEditBackground
+from ..GUI_instances import clearMonitor
+from ...paths import PATHS
from ...utilities import SUPPORTED_EXTENSION
+from ...elements import Variable as Variable_og
+from ...variables import Variable
class Monitor(QtWidgets.QMainWindow):
- def __init__(self, item: QtWidgets.QTreeWidgetItem):
- self.gui = item if isinstance(item, QtWidgets.QTreeWidgetItem) else None
- self.item = item
- self.variable = item.variable
-
- self._font_size = get_font_size() + 1
+ def __init__(self,
+ variable: Union[Variable, Variable_og],
+ has_parent: bool = False):
+ self.has_parent = has_parent # Only for closeEvent
+ self.variable = variable
+ self._font_size = get_font_size()
# Configuration of the window
super().__init__()
ui_path = os.path.join(os.path.dirname(__file__), 'interface.ui')
uic.loadUi(ui_path, self)
- self.setWindowTitle(f"AUTOLAB - Monitor: Variable {self.variable.address()}")
- self.setWindowIcon(QtGui.QIcon(icons['monitor']))
+ self.setWindowTitle(f"AUTOLAB - Monitor: {self.variable.address()}")
+ self.setWindowIcon(icons['monitor'])
# Queue
self.queue = queue.Queue()
self.timer = QtCore.QTimer(self)
@@ -48,7 +52,7 @@ def __init__(self, item: QtWidgets.QTreeWidgetItem):
setLineEditBackground(
self.windowLength_lineEdit, 'synced', self._font_size)
- self.xlabel = '' # defined in data according to data type
+ self.xlabel = 'Time(s)' # Is changed to x if ndarray or dataframe
self.ylabel = f'{self.variable.address()}' # OPTIMIZE: could depend on 1D or 2D
if self.variable.unit is not None:
@@ -61,22 +65,11 @@ def __init__(self, item: QtWidgets.QTreeWidgetItem):
self.delay_lineEdit, 'edited', self._font_size))
setLineEditBackground(self.delay_lineEdit, 'synced', self._font_size)
- # Pause
self.pauseButton.clicked.connect(self.pauseButtonClicked)
-
- # Save
self.saveButton.clicked.connect(self.saveButtonClicked)
-
- # Clear
self.clearButton.clicked.connect(self.clearButtonClicked)
-
- # Mean
self.mean_checkBox.clicked.connect(self.mean_checkBoxClicked)
-
- # Min
self.min_checkBox.clicked.connect(self.min_checkBoxClicked)
-
- # Max
self.max_checkBox.clicked.connect(self.max_checkBoxClicked)
# Managers
@@ -90,6 +83,31 @@ def __init__(self, item: QtWidgets.QTreeWidgetItem):
self.monitorManager.start()
self.timer.start()
+ # Use to pause monitor on scan start
+ self.pause_on_scan = False
+ self.start_on_scan = False
+ if self.has_parent:
+ self.pause_on_scan_checkBox.clicked.connect(
+ self.pause_on_scan_checkBoxClicked)
+ self.start_on_scan_checkBox.clicked.connect(
+ self.start_on_scan_checkBoxClicked)
+ else:
+ self.pause_on_scan_checkBox.hide()
+ self.start_on_scan_checkBox.hide()
+
+ for splitter in (self.splitter, ):
+ for i in range(splitter.count()):
+ handle = splitter.handle(i)
+ handle.setStyleSheet("background-color: #DDDDDD;")
+ handle.installEventFilter(self)
+
+ def eventFilter(self, obj, event):
+ if event.type() == QtCore.QEvent.Enter:
+ obj.setStyleSheet("background-color: #AAAAAA;") # Hover color
+ elif event.type() == QtCore.QEvent.Leave:
+ obj.setStyleSheet("background-color: #DDDDDD;") # Normal color
+ return super().eventFilter(obj, event)
+
def sync(self):
""" This function updates the data and then the figure.
Function called by the time """
@@ -126,14 +144,14 @@ def saveButtonClicked(self):
# Ask the filename of the output data
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self, caption="Save data", directory=os.path.join(
- paths.USER_LAST_CUSTOM_FOLDER,
+ PATHS['last_folder'],
f'{self.variable.address()}_monitor.txt'),
filter=SUPPORTED_EXTENSION)
path = os.path.dirname(filename)
# Save the given path for future, the data and the figure if the path provided is valid
if path != '':
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
self.setStatus('Saving data...', 5000)
try:
@@ -175,15 +193,24 @@ def max_checkBoxClicked(self):
if len(xlist) > 0: self.figureManager.update(xlist, ylist)
+ def pause_on_scan_checkBoxClicked(self):
+ """ Change pause_on_scan variable """
+ self.pause_on_scan = self.pause_on_scan_checkBox.isChecked()
+
+ def start_on_scan_checkBoxClicked(self):
+ """ Change start_on_scan variable """
+ self.start_on_scan = self.start_on_scan_checkBox.isChecked()
+
def closeEvent(self, event):
""" This function does some steps before the window is really killed """
self.monitorManager.close()
self.timer.stop()
- if hasattr(self.item, 'clearMonitor'): self.item.clearMonitor()
self.figureManager.fig.deleteLater() # maybe not useful for monitor but was source of crash in scanner if didn't close
self.figureManager.figMap.deleteLater()
- if self.gui is None:
+ clearMonitor(self.variable)
+
+ if not self.has_parent:
import pyqtgraph as pg
try:
# Prevent 'RuntimeError: wrapped C/C++ object of type ViewBox has been deleted' when reloading gui
@@ -195,10 +222,11 @@ def closeEvent(self, event):
for children in self.findChildren(QtWidgets.QWidget):
children.deleteLater()
+
super().closeEvent(event)
- if self.gui is None:
- QtWidgets.QApplication.quit() # close the monitor app
+ if not self.has_parent:
+ QtWidgets.QApplication.quit() # close the app
def windowLengthChanged(self):
""" This function start the update of the window length in the data manager
@@ -240,7 +268,7 @@ def updateDelayGui(self):
self.delay_lineEdit.setText(f'{value:g}')
setLineEditBackground(self.delay_lineEdit, 'synced', self._font_size)
- def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
+ def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
""" Modify the message displayed in the status bar and add error message to logger """
self.statusBar.showMessage(message, timeout)
if not stdout: print(message, file=sys.stderr)
diff --git a/autolab/core/gui/monitoring/monitor.py b/autolab/core/gui/monitoring/monitor.py
index b95f3106..0a59291e 100644
--- a/autolab/core/gui/monitoring/monitor.py
+++ b/autolab/core/gui/monitoring/monitor.py
@@ -4,6 +4,7 @@
@author: qchat
"""
+from typing import Union
import time
import threading
from queue import Queue
@@ -12,7 +13,8 @@
import pandas as pd
from qtpy import QtCore, QtWidgets
-from ...devices import Device
+from ...variables import Variable
+from ...elements import Variable as Variable_og
class MonitorManager:
@@ -67,7 +69,7 @@ class MonitorThread(QtCore.QThread):
errorSignal = QtCore.Signal(object)
- def __init__(self, variable: Device, queue: Queue):
+ def __init__(self, variable: Union[Variable, Variable_og], queue: Queue):
super().__init__()
self.variable = variable
diff --git a/autolab/core/gui/plotting/data.py b/autolab/core/gui/plotting/data.py
index f0205ec9..90ba5822 100644
--- a/autolab/core/gui/plotting/data.py
+++ b/autolab/core/gui/plotting/data.py
@@ -5,6 +5,7 @@
@author: jonathan based on qchat
"""
+from typing import List, Union, Any
import os
import sys
import csv
@@ -17,11 +18,14 @@
except:
no_default = None
-from qtpy import QtCore, QtWidgets
+from qtpy import QtWidgets
-from ... import paths
-from ... import config
-from ... import utilities
+from ...paths import PATHS
+from ...config import load_config
+from ...utilities import data_to_dataframe, SUPPORTED_EXTENSION
+from ...devices import DEVICES, get_element_by_address, list_devices
+from ...elements import Variable as Variable_og
+from ...variables import list_variables, get_variable, Variable
def find_delimiter(filename):
@@ -62,7 +66,8 @@ def find_header(filename, sep=no_default, skiprows=None):
else:
if skiprows == 1:
try:
- df_columns = pd.read_csv(filename, sep=sep, header="infer", skiprows=0, nrows=0)
+ df_columns = pd.read_csv(filename, sep=sep, header="infer",
+ skiprows=0, nrows=0)
except Exception:
pass
else:
@@ -74,13 +79,16 @@ def find_header(filename, sep=no_default, skiprows=None):
try:
first_row = df.iloc[0].values.astype("float")
- return ("infer", skiprows, no_default) if tuple(first_row) == tuple([i for i in range(len(first_row))]) else (None, skiprows, no_default)
+ return (("infer", skiprows, no_default)
+ if tuple(first_row) == tuple([i for i in range(len(first_row))])
+ else (None, skiprows, no_default))
except:
pass
df_header = pd.read_csv(filename, sep=sep, nrows=5, skiprows=skiprows)
- return ("infer", skiprows, no_default) if tuple(df.dtypes) != tuple(df_header.dtypes) else (None, skiprows, no_default)
-
+ return (("infer", skiprows, no_default)
+ if tuple(df.dtypes) != tuple(df_header.dtypes)
+ else (None, skiprows, no_default))
def importData(filename):
@@ -90,52 +98,52 @@ def importData(filename):
sep = find_delimiter(filename)
(header, skiprows, columns) = find_header(filename, sep, skiprows)
try:
- data = pd.read_csv(filename, sep=sep, header=header, skiprows=skiprows, names=columns)
+ data = pd.read_csv(filename, sep=sep, header=header,
+ skiprows=skiprows, names=columns)
except TypeError:
- data = pd.read_csv(filename, sep=sep, header=header, skiprows=skiprows, names=None) # for pandas 1.2: names=None but sep=no_default
+ data = pd.read_csv(filename, sep=sep, header=header,
+ skiprows=skiprows, names=None) # for pandas 1.2: names=None but sep=no_default
except:
- data = pd.read_csv(filename, sep="\t", header=header, skiprows=skiprows, names=columns)
+ data = pd.read_csv(filename, sep="\t", header=header,
+ skiprows=skiprows, names=columns)
assert len(data) != 0, "Can't import empty DataFrame"
- data = utilities.formatData(data)
+ data = data_to_dataframe(data)
return data
+class DataManager:
-class DataManager :
-
- def __init__(self,gui):
+ def __init__(self, gui: QtWidgets.QMainWindow):
self.gui = gui
-
self._clear()
-
self.overwriteData = True
- plotter_config = config.load_config("plotter")
- if 'device' in plotter_config.sections() and 'address' in plotter_config['device']:
- self.deviceValue = str(plotter_config['device']['address'])
+ plotter_config = load_config("plotter_config")
+ if ('device' in plotter_config.sections()
+ and 'address' in plotter_config['device']):
+ self.variable_address = str(plotter_config['device']['address'])
else:
- self.deviceValue = ''
+ self.variable_address = ''
def _clear(self):
self.datasets = []
self.last_variables = []
- def setOverwriteData(self, value):
+ def setOverwriteData(self, value: bool):
self.overwriteData = bool(value)
- def getDatasetsNames(self):
+ def getDatasetsNames(self) -> List[str]:
names = []
for dataset in self.datasets:
names.append(dataset.name)
return names
@staticmethod
- def getUniqueName(name, names_list):
-
- """ This function adds a number next to basename in case this basename is already taken """
-
+ def getUniqueName(name: str, names_list: List[str]):
+ """ This function adds a number next to basename in case this basename
+ is already taken """
raw_name, extension = os.path.splitext(name)
if extension in (".txt", ".csv", ".dat"):
@@ -146,48 +154,51 @@ def getUniqueName(name, names_list):
putext = False
compt = 0
- while True :
- if name in names_list :
+ while True:
+ if name in names_list:
compt += 1
if putext:
- name = basename+'_'+str(compt)+extension
+ name = basename+'_' + str(compt) + extension
else:
- name = basename+'_'+str(compt)
- else :
+ name = basename + '_' + str(compt)
+ else:
break
return name
- def setDeviceValue(self,value):
- """ This function set the value of the target device value """
-
- try:
- self.getDeviceName(value)
- except:
- raise NameError(f"The given value '{value}' is not a device variable or the device is closed")
- else:
- self.deviceValue = value
+ def set_variable_address(self, value: str):
+ """ This function set the address of the target variable """
+ # Can raise errors
+ self.getVariable(value)
+ # If no errors
+ self.variable_address = value
- def getDeviceValue(self):
+ def get_variable_address(self) -> str:
""" This function returns the value of the target device value """
+ return self.variable_address
- return self.deviceValue
-
- def getDeviceName(self, name):
+ def getVariable(self, name: str) -> Union[Variable, Variable_og]:
""" This function returns the name of the target device value """
+ assert name != '', 'Need to provide a variable name'
+ device = name.split(".")[0]
+
+ if device in list_variables():
+ assert device == name, f"Can only use '{device}' directly"
+ variable = get_variable(name)
+ elif device in list_devices():
+ assert device in DEVICES, f"Device '{device}' is closed"
+ variable = get_element_by_address(name) # Can raise 'name' not found in module 'device'
+ assert isinstance(variable, Variable_og), (
+ f"Need a variable but '{name}' is a {str(type(variable).__name__)}")
+ assert variable.readable, f"Variable '{name}' is not readable"
+ var_type = str(variable.type).split("'")[1]
+ assert variable.type in (int, float, np.ndarray, pd.DataFrame), (
+ f"Datatype '{var_type}' of '{name}' is not supported")
+ else:
+ assert False, f"'{device}' is neither a device nor a variable"
- try:
- module_name, *submodules_name, variable_name = name.split(".")
- module = self.gui.mainGui.tree.findItems(module_name, QtCore.Qt.MatchExactly)[0].module
- for submodule_name in submodules_name:
- module = module.get_module(submodule_name)
- variable = module.get_variable(variable_name)
- except:
- raise NameError(f"The given value '{name}' is not a device variable or the device is closed")
return variable
-
def data_comboBoxClicked(self):
-
""" This function select a dataset """
if len(self.datasets) == 0:
self.gui.save_pushButton.setEnabled(False)
@@ -197,58 +208,63 @@ def data_comboBoxClicked(self):
self.updateDisplayableResults()
def importActionClicked(self):
-
""" This function prompts the user for a dataset filename,
and import the dataset"""
-
filenames = QtWidgets.QFileDialog.getOpenFileNames(
- self.gui, "Import data file", paths.USER_LAST_CUSTOM_FOLDER,
- filter=utilities.SUPPORTED_EXTENSION)[0]
+ self.gui, "Import data file", PATHS['last_folder'],
+ filter=SUPPORTED_EXTENSION)[0]
if not filenames:
- return
+ return None
else:
self.importAction(filenames)
- def importAction(self, filenames):
+ def importAction(self, filenames: List[str]):
dataset = None
for i, filename in enumerate(filenames):
-
- try :
+ try:
dataset = self.importData(filename)
- except Exception as error:
- self.gui.setStatus(f"Impossible to load data from {filename}: {error}",10000, False)
+ except Exception as e:
+ self.gui.setStatus(
+ f"Impossible to load data from {filename}: {e}",
+ 10000, False)
if len(filenames) != 1:
- print(f"Impossible to load data from {filename}: {error}", file=sys.stderr)
+ print(f"Impossible to load data from {filename}: {e}",
+ file=sys.stderr)
else:
- self.gui.setStatus(f"File {filename} loaded successfully",5000)
+ self.gui.setStatus(f"File {filename} loaded successfully", 5000)
if dataset is not None:
self.gui.figureManager.start(dataset)
path = os.path.dirname(filename)
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
- def importDeviceData(self, deviceVariable):
+ def importDeviceData(self, variable: Union[Variable, Variable_og, pd.DataFrame, Any]):
""" This function open the data of the provided device """
-
- data = deviceVariable()
-
- data = utilities.formatData(data)
+ if isinstance(variable, pd.DataFrame):
+ name = variable.name if hasattr(variable, 'name') else 'dataframe'
+ data = variable
+ elif isinstance(variable, (Variable, Variable_og)):
+ name = variable.address()
+ data = variable() # read value
+ else:
+ name = 'data'
+ data = variable
+ data = data_to_dataframe(data) # format value
if self.overwriteData:
- data_name = self.deviceValue
+ data_name = name
else:
names_list = self.getDatasetsNames()
- data_name = DataManager.getUniqueName(self.deviceValue, names_list)
+ data_name = DataManager.getUniqueName(
+ name, names_list)
dataset = self.newDataset(data_name, data)
return dataset
- def importData(self, filename):
+ def importData(self, filename: str):
""" This function open the data with the provided filename """
# OPTIMIZE: could add option to choose in GUI all options
-
data = importData(filename)
-
name = os.path.basename(filename)
if self.overwriteData:
@@ -273,20 +289,18 @@ def _addData(self, new_dataset):
# Prepare a new dataset in the plotter
self.gui.dataManager.addDataset(new_dataset)
- def getData(self,nbDataset,varList, selectedData=0):
+ def getData(self, nbDataset: int, var_list: List[str], selectedData: int = 0):
""" This function returns to the figure manager the required data """
-
dataList = []
-
- for i in range(selectedData, nbDataset+selectedData) :
- if i < len(self.datasets) :
+ for i in range(selectedData, nbDataset+selectedData):
+ if i < len(self.datasets):
dataset = self.datasets[-(i+1)]
try:
- data = dataset.getData(varList)
+ data = dataset.getData(var_list)
except:
data = None
dataList.append(data)
- else :
+ else:
break
dataList.reverse()
@@ -295,24 +309,22 @@ def getData(self,nbDataset,varList, selectedData=0):
def saveButtonClicked(self):
""" This function is called when the save button is clicked.
It asks a path and starts the procedure to save the data """
-
dataset = self.getLastSelectedDataset()
- if dataset is not None :
-
- filename, _ = QtWidgets.QFileDialog.getSaveFileName(
- self.gui, caption="Save data",
- directory=paths.USER_LAST_CUSTOM_FOLDER,
- filter=utilities.SUPPORTED_EXTENSION)
+ if dataset is not None:
+ filename = QtWidgets.QFileDialog.getSaveFileName(
+ self.gui, caption="Save data", directory=PATHS['last_folder'],
+ filter=SUPPORTED_EXTENSION)[0]
path = os.path.dirname(filename)
- if path != '' :
- paths.USER_LAST_CUSTOM_FOLDER = path
- self.gui.setStatus('Saving data...',5000)
+ if path != '':
+ PATHS['last_folder'] = path
+ self.gui.setStatus('Saving data...', 5000)
dataset.save(filename)
self.gui.figureManager.save(filename)
- self.gui.setStatus(f'Last dataset successfully saved in {filename}',5000)
+ self.gui.setStatus(
+ f'Last dataset successfully saved in {filename}', 5000)
def clear(self):
""" Clear displayed dataset """
@@ -325,8 +337,8 @@ def clear(self):
self.deleteData(dataset)
self.gui.data_comboBox.removeItem(index)
self.gui.setStatus(f"Removed {data_name}", 5000)
- except Exception as error:
- self.gui.setStatus(f"Can't delete: {error}", 10000, False)
+ except Exception as e:
+ self.gui.setStatus(f"Can't delete: {e}", 10000, False)
pass
if self.gui.data_comboBox.count() == 0:
@@ -334,7 +346,8 @@ def clear(self):
return
else:
- if index == (nbr_data-1) and index != 0: # if last point but exist other data takes previous data else keep index
+ # if last point but exist other data takes previous data else keep index
+ if index == (nbr_data-1) and index != 0:
index -= 1
self.gui.data_comboBox.setCurrentIndex(index)
@@ -344,7 +357,6 @@ def clear(self):
def clear_all(self):
""" This reset any recorded data, and the GUI accordingly """
-
self._clear()
self.gui.figureManager.clearData()
@@ -357,70 +369,71 @@ def clear_all(self):
def deleteData(self, dataset):
""" This function remove dataset from the datasets"""
-
self.datasets.remove(dataset)
def getLastSelectedDataset(self):
""" This return the current (last selected) dataset """
-
if len(self.datasets) > 0:
return self.datasets[self.gui.data_comboBox.currentIndex()]
def addDataset(self, dataset):
""" This function add the given dataset to datasets list """
-
self.datasets.append(dataset)
- def newDataset(self, name, data):
+ def newDataset(self, name: str, data: pd.DataFrame):
""" This function creates a new dataset """
-
- dataset = Dataset(self.gui, name, data)
+ dataset = Dataset(name, data)
self._addData(dataset)
return dataset
def updateDisplayableResults(self):
- """ This function update the combobox in the GUI that displays the names of
- the results that can be plotted """
-
+ """ This function update the combobox in the GUI that displays the
+ names of the results that can be plotted """
dataset = self.getLastSelectedDataset()
variables_list = list(dataset.data.columns)
if variables_list != self.last_variables:
self.last_variables = variables_list
- resultNamesList = []
+ result_names = []
- for resultName in variables_list:
+ for result_name in variables_list:
try:
- float(dataset.data.iloc[0][resultName])
- resultNamesList.append(resultName)
- except Exception as er:
- self.gui.setStatus(f"Can't plot data: {er}", 10000, False)
- return
+ float(dataset.data.iloc[0][result_name])
+ result_names.append(result_name)
+ except Exception as e:
+ self.gui.setStatus(f"Can't plot data: {e}", 10000, False)
+ return None
variable_x = self.gui.variable_x_comboBox.currentText()
variable_y = self.gui.variable_y_comboBox.currentText()
# If can, put back previous x and y variable in combobox
- is_id = variables_list[0] == "id" and len(variables_list) > 2
-
- if is_id: # move id form begin to end
- name=resultNamesList.pop(0)
- resultNamesList.append(name)
+ is_id = (len(variables_list) != 0
+ and variables_list[0] == "id"
+ and len(variables_list) > 2)
- AllItems = [self.gui.variable_x_comboBox.itemText(i) for i in range(self.gui.variable_x_comboBox.count())]
+ if is_id:
+ # move id form begin to end
+ name=result_names.pop(0)
+ result_names.append(name)
- if resultNamesList != AllItems: # only refresh if change labels, to avoid gui refresh that prevent user to click on combobox
+ AllItems = [self.gui.variable_x_comboBox.itemText(i)
+ for i in range(self.gui.variable_x_comboBox.count())]
+ # only refresh if change labels, to avoid gui refresh that prevent user to click on combobox
+ if result_names != AllItems:
self.gui.variable_x_comboBox.clear()
self.gui.variable_y_comboBox.clear()
- self.gui.variable_x_comboBox.addItems(resultNamesList) # slow (0.25s)
+ self.gui.variable_x_comboBox.addItems(result_names) # slow (0.25s)
+
+ if len(result_names) != 0:
+ # move first item to end (only for y axis)
+ name=result_names.pop(0)
+ result_names.append(name)
- # move first item to end (only for y axis)
- name=resultNamesList.pop(0)
- resultNamesList.append(name)
- self.gui.variable_y_comboBox.addItems(resultNamesList)
+ self.gui.variable_y_comboBox.addItems(result_names)
if variable_x in variables_list:
index = self.gui.variable_x_comboBox.findText(variable_x)
@@ -437,34 +450,28 @@ def updateDisplayableResults(self):
self.gui.figureManager.reloadData() # 0.1s
-
class Dataset():
- def __init__(self,gui,name, data):
+ def __init__(self, name: str, data: pd.DataFrame):
- self.gui = gui
self.name = name
self.data = data
- def getData(self,varList):
- """ This function returns a dataframe with two columns : the parameter value,
- and the requested result value """
-
- if varList[0] == varList[1] : return self.data.loc[:,[varList[0]]]
- else : return self.data.loc[:,varList]
+ def getData(self, var_list: List[str]):
+ """ This function returns a dataframe with two columns:
+ the parameter value, and the requested result value """
+ if var_list[0] == var_list[1]: return self.data.loc[:, [var_list[0]]]
+ else: return self.data.loc[:, var_list]
def update(self, dataset):
""" Change name and data of this dataset """
-
self.name = dataset.name
self.data = dataset.data
- def save(self,filename):
+ def save(self,filename: str):
""" This function saved the dataset in the provided path """
-
- self.data.to_csv(filename,index=False)
+ self.data.to_csv(filename, index=False)
def __len__(self):
""" Returns the number of data point of this dataset """
-
return len(self.data)
diff --git a/autolab/core/gui/plotting/figure.py b/autolab/core/gui/plotting/figure.py
index bab3ff27..538e5034 100644
--- a/autolab/core/gui/plotting/figure.py
+++ b/autolab/core/gui/plotting/figure.py
@@ -7,14 +7,16 @@
import os
import pyqtgraph as pg
-import pyqtgraph.exporters
+import pyqtgraph.exporters # Needed for pg.exporters.ImageExporter
+
+from qtpy import QtWidgets
from ..GUI_utilities import pyqtgraph_fig_ax
class FigureManager:
- def __init__(self, gui):
+ def __init__(self, gui: QtWidgets.QMainWindow):
self.gui = gui
self.curves = []
@@ -29,17 +31,17 @@ def __init__(self, gui):
def start(self, new_dataset=None):
""" This function display data and ajust buttons """
try:
- resultNamesList = [dataset.name for dataset in self.gui.dataManager.datasets]
- AllItems = [self.gui.data_comboBox.itemText(i) for i in range(self.gui.data_comboBox.count())]
+ result_names = [dataset.name for dataset in self.gui.dataManager.datasets]
+ all_items = [self.gui.data_comboBox.itemText(i) for i in range(self.gui.data_comboBox.count())]
index = self.gui.data_comboBox.currentIndex()
- if resultNamesList != AllItems: # only refresh if change labels, to avoid gui refresh that prevent user to click on combobox
+ if result_names != all_items: # only refresh if change labels, to avoid gui refresh that prevent user to click on combobox
self.gui.data_comboBox.clear()
- self.gui.data_comboBox.addItems(resultNamesList) # slow (0.25s)
+ self.gui.data_comboBox.addItems(result_names) # slow (0.25s)
if new_dataset is None:
- if (index + 1) > len(resultNamesList) or index == -1: index = 0
+ if (index + 1) > len(result_names) or index == -1: index = 0
self.gui.data_comboBox.setCurrentIndex(index)
else:
index = self.gui.data_comboBox.findText(new_dataset.name)
@@ -57,21 +59,19 @@ def start(self, new_dataset=None):
self.gui.setStatus(f'Data {data_name} plotted!', 5000)
except Exception as e:
- self.gui.setStatus(f'ERROR The data cannot be plotted with the given dataset: {str(e)}',
- 10000, False)
+ self.gui.setStatus(
+ f'ERROR The data cannot be plotted with the given dataset: {e}',
+ 10000, False)
# AXE LABEL
###########################################################################
- def getLabel(self, axe: str):
- """ This function get the label of the given axis """
- return getattr(self.gui, f"variable_{axe}_comboBox").currentText()
-
def setLabel(self, axe: str, value: str):
""" This function changes the label of the given axis """
- axes = {'x':'bottom', 'y':'left'}
+ axes = {'x': 'bottom', 'y': 'left'}
if value == '': value = ' '
- self.ax.setLabel(axes[axe], value, **{'color':0.4, 'font-size': '12pt'})
+ self.ax.setLabel(axes[axe], value, **{'color': pg.getConfigOption("foreground"),
+ 'font-size': '12pt'})
# PLOT DATA
@@ -79,7 +79,7 @@ def setLabel(self, axe: str, value: str):
def clearData(self):
""" This function removes any plotted curves """
- for curve in self.curves :
+ for curve in self.curves:
self.ax.removeItem(curve)
self.curves = []
@@ -90,8 +90,8 @@ def reloadData(self):
self.clearData()
# Get current displayed result
- variable_x = self.getLabel("x")
- variable_y = self.getLabel("y")
+ variable_x = self.gui.variable_x_comboBox.currentText()
+ variable_y = self.gui.variable_y_comboBox.currentText()
data_id = int(self.gui.data_comboBox.currentIndex()) + 1
data_len = len(self.gui.dataManager.datasets)
selectedData = data_len - data_id
@@ -103,16 +103,17 @@ def reloadData(self):
try :
# OPTIMIZE: currently load all data and plot more than self.nbtraces if in middle
# Should change to only load nbtraces and plot nbtraces
- data = self.gui.dataManager.getData(data_len, [variable_x,variable_y], selectedData=0)
+ data = self.gui.dataManager.getData(
+ data_len, [variable_x,variable_y], selectedData=0)
# data = self.gui.dataManager.getData(self.nbtraces,[variable_x,variable_y], selectedData=selectedData)
- except :
+ except:
data = None
# Plot them
- if data is not None :
+ if data is not None:
- for i in range(len(data)) :
- if i != (data_id-1):
+ for i in range(len(data)):
+ if i != (data_id - 1):
# Data
subdata = data[i]
if subdata is None:
@@ -123,37 +124,44 @@ def reloadData(self):
y = subdata.loc[:,variable_y]
# Apprearance:
- color = 'k'
- alpha = (self.nbtraces-abs(data_id-1-i))/self.nbtraces
+ color = pg.getConfigOption("foreground")
+ alpha = (self.nbtraces - abs(data_id - 1 - i)) / self.nbtraces
if alpha < 0: alpha = 0
# Plot
# OPTIMIZE: keep previous style to avoid overwriting it everytime
- if i < (data_id-1):
+ if i < (data_id - 1):
if len(x) > 300:
curve = self.ax.plot(x, y, pen=color)
curve.setAlpha(alpha, False)
else:
- curve = self.ax.plot(x, y, symbol='x', symbolPen=color, symbolSize=10, pen=color, symbolBrush=color)
+ curve = self.ax.plot(
+ x, y, symbol='x', symbolPen=color,
+ symbolSize=10, pen=color, symbolBrush=color)
curve.setAlpha(alpha, False)
- elif i > (data_id-1):
+ elif i > (data_id - 1):
if len(x) > 300:
- curve = self.ax.plot(x, y, pen=pg.mkPen(color=color, style=pg.QtCore.Qt.DashLine))
+ curve = self.ax.plot(
+ x, y, pen=pg.mkPen(color=color,
+ style=pg.QtCore.Qt.DashLine))
curve.setAlpha(alpha, False)
else:
- curve = self.ax.plot(x, y, symbol='x', symbolPen=color, symbolSize=10, pen=pg.mkPen(color=color, style=pg.QtCore.Qt.DashLine), symbolBrush=color)
+ curve = self.ax.plot(
+ x, y, symbol='x', symbolPen=color, symbolSize=10,
+ pen=pg.mkPen(color=color, style=pg.QtCore.Qt.DashLine),
+ symbolBrush=color)
curve.setAlpha(alpha, False)
self.curves.append(curve)
# Data
- i = (data_id-1)
+ i = (data_id - 1)
subdata = data[i]
if subdata is not None:
subdata = subdata.astype(float)
- x = subdata.loc[:,variable_x]
- y = subdata.loc[:,variable_y]
+ x = subdata.loc[:, variable_x]
+ y = subdata.loc[:, variable_y]
# Apprearance:
color = '#1f77b4'
@@ -164,7 +172,9 @@ def reloadData(self):
curve = self.ax.plot(x, y, pen=color)
curve.setAlpha(alpha, False)
else:
- curve = self.ax.plot(x, y, symbol='x', symbolPen=color, symbolSize=10, pen=color, symbolBrush=color)
+ curve = self.ax.plot(
+ x, y, symbol='x', symbolPen=color, symbolSize=10,
+ pen=color, symbolBrush=color)
curve.setAlpha(alpha, False)
self.curves.append(curve)
@@ -174,7 +184,7 @@ def reloadData(self):
# SAVE FIGURE
###########################################################################
- def save(self,filename):
+ def save(self, filename: str):
""" This function save the figure with the provided filename """
raw_name, extension = os.path.splitext(filename)
diff --git a/autolab/core/gui/plotting/interface.ui b/autolab/core/gui/plotting/interface.ui
index 28d7f16e..a17b55dc 100644
--- a/autolab/core/gui/plotting/interface.ui
+++ b/autolab/core/gui/plotting/interface.ui
@@ -10,20 +10,32 @@
734
-
-
- 9
-
-
-
-
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ Qt::Vertical
-
-
+
+
+ Qt::Horizontal
+
+
+
+
@@ -33,9 +45,15 @@
20
+
+ 0
+ 0
+
+ 0
+ 0
@@ -53,11 +71,6 @@
-
-
- 9
-
- Open data
@@ -152,27 +165,28 @@
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
-
+ QFrame::StyledPanel
-
-
-
-
- Auto get data
-
-
-
+
-
-
- 9
-
- Display data from the given variable address
@@ -181,42 +195,10 @@
-
-
-
- Variable address i.g. ct400.scan.data
-
-
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 0
- 20
-
-
-
-
-
-
-
-
- 75
- true
-
-
+
+
- Device :
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+ Address:
@@ -228,12 +210,6 @@
0
-
-
- 50
- 0
-
- 75
@@ -251,25 +227,10 @@
-
-
-
-
- 9
-
-
-
- Delay [s] :
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
-
-
+
+
- Variable address
+ Auto get data
@@ -286,6 +247,32 @@
+
+
+
+
+ 75
+ true
+
+
+
+ Variable:
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+
+
+
+ Delay [s] :
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
@@ -294,54 +281,35 @@
-
-
-
-
- QComboBox::AdjustToContents
-
-
-
-
-
-
-
- 75
- true
-
-
-
- Y axis
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
-
-
-
- QComboBox::AdjustToContents
-
-
-
-
-
-
-
- 75
- true
-
-
-
- X axis
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
+
+
+
+
+
+
+
+ 75
+ true
+
+
+
+ X axis
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
+ QComboBox::AdjustToContents
+
+
+
+
-
+ Qt::Horizontal
@@ -354,70 +322,81 @@
+
+
+
+
+
+
+ 75
+ true
+
+
+
+ Y axis
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
+ QComboBox::AdjustToContents
+
+
+
+
+
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Nb traces :
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Number of visible traces
-
-
- 1
-
-
- Qt::AlignCenter
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Nb traces
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Number of visible traces
+
+
+ 1
+
+
+ Qt::AlignCenter
+
+
+
+
@@ -463,12 +442,9 @@
clear_pushButtondata_comboBoxoverwriteDataButton
- device_lineEditplotDataButtondelay_lineEditauto_plotDataButton
- variable_x_comboBox
- variable_y_comboBox
diff --git a/autolab/core/gui/plotting/main.py b/autolab/core/gui/plotting/main.py
index 2259fa66..ff1a532b 100644
--- a/autolab/core/gui/plotting/main.py
+++ b/autolab/core/gui/plotting/main.py
@@ -9,8 +9,9 @@
import queue
import time
import uuid
-from typing import Any, Type
+from typing import Type, Union, Any
+import pandas as pd
from qtpy import QtCore, QtWidgets, uic, QtGui
from .figure import FigureManager
@@ -18,34 +19,37 @@
from .thread import ThreadManager
from .treewidgets import TreeWidgetItemModule
from ..icons import icons
-from ... import devices
-from ... import config
-from ..GUI_utilities import get_font_size, setLineEditBackground
+from ..GUI_utilities import get_font_size, setLineEditBackground, MyLineEdit
+from ..GUI_instances import clearPlotter, closePlotter
+from ...devices import list_devices
+from ...elements import Variable as Variable_og
+from ...variables import Variable
+from ...config import load_config
class MyQTreeWidget(QtWidgets.QTreeWidget):
- reorderSignal = QtCore.Signal(object)
-
- def __init__(self, parent, plotter):
- self.plotter = plotter
+ def __init__(self, gui, parent=None):
+ self.gui = gui
super().__init__(parent)
self.setAcceptDrops(True)
def dropEvent(self, event):
""" This function is used to add a plugin to the plotter """
- variable = event.source().last_drag
- if isinstance(variable, str):
- self.plotter.addPlugin(variable)
+ plugin_name = event.source().last_drag
+ if isinstance(plugin_name, str):
+ self.gui.addPlugin(plugin_name, plugin_name)
self.setGraphicsEffect(None)
def dragEnterEvent(self, event):
if (event.source() is self) or (
- hasattr(event.source(), "last_drag") and isinstance(event.source().last_drag, str)):
+ hasattr(event.source(), "last_drag")
+ and isinstance(event.source().last_drag, str)):
event.accept()
- shadow = QtWidgets.QGraphicsDropShadowEffect(blurRadius=25, xOffset=3, yOffset=3)
+ shadow = QtWidgets.QGraphicsDropShadowEffect(
+ blurRadius=25, xOffset=3, yOffset=3)
self.setGraphicsEffect(shadow)
else:
event.ignore()
@@ -53,24 +57,53 @@ def dragEnterEvent(self, event):
def dragLeaveEvent(self, event):
self.setGraphicsEffect(None)
+ def keyPressEvent(self, event):
+ if (event.key() == QtCore.Qt.Key_C
+ and event.modifiers() == QtCore.Qt.ControlModifier):
+ self.copy_item(event)
+ else:
+ super().keyPressEvent(event)
+
+ def copy_item(self, event):
+ if len(self.selectedItems()) == 0:
+ super().keyPressEvent(event)
+ return None
+ item = self.selectedItems()[0] # assume can select only one item
+ if hasattr(item, 'variable'):
+ text = item.variable.address()
+ elif hasattr(item, 'action'):
+ text = item.action.address()
+ elif hasattr(item, 'module'):
+ if hasattr(item.module, 'address'):
+ text = item.module.address()
+ else:
+ text = item.name
+ else:
+ print(f'Should not be possible: {item}')
+ super().keyPressEvent(event)
+ return None
+
+ # Copy the text to the system clipboard
+ clipboard = QtWidgets.QApplication.clipboard()
+ clipboard.setText(text)
class Plotter(QtWidgets.QMainWindow):
- def __init__(self, mainGui):
+ def __init__(self, has_parent: bool = False):
self.active = False
- self.mainGui = mainGui
+ self.has_parent = has_parent # Only for closeEvent
self.all_plugin_list = []
self.active_plugin_dict = {}
- self._font_size = get_font_size() + 1
+ self._font_size = get_font_size()
# Configuration of the window
super().__init__()
ui_path = os.path.join(os.path.dirname(__file__), 'interface.ui')
uic.loadUi(ui_path, self)
self.setWindowTitle("AUTOLAB - Plotter")
- self.setWindowIcon(QtGui.QIcon(icons['plotter']))
+ self.setWindowIcon(icons['plotter'])
# Loading of the different centers
self.figureManager = FigureManager(self)
@@ -104,36 +137,40 @@ def __init__(self, mainGui):
setLineEditBackground(self.nbTraces_lineEdit, 'synced', self._font_size)
self.variable_x_comboBox.currentIndexChanged.connect(
- self.variableChanged)
+ self.axisChanged)
self.variable_y_comboBox.currentIndexChanged.connect(
- self.variableChanged)
-
- if self.mainGui is not None:
- self.device_lineEdit.setText(f'{self.dataManager.deviceValue}')
- self.device_lineEdit.returnPressed.connect(self.deviceChanged)
- self.device_lineEdit.textEdited.connect(lambda: setLineEditBackground(
- self.device_lineEdit, 'edited', self._font_size))
- setLineEditBackground(self.device_lineEdit, 'synced', self._font_size)
-
- # Plot button
- self.plotDataButton.clicked.connect(self.refreshPlotData)
-
- # Timer
- self.timer_time = 0.5 # This plotter is not meant for fast plotting like the monitor, be aware it may crash with too high refreshing rate
- self.timer = QtCore.QTimer(self)
- self.timer.setInterval(int(self.timer_time*1000)) # ms
- self.timer.timeout.connect(self.autoRefreshPlotData)
-
- self.auto_plotDataButton.clicked.connect(self.autoRefreshChanged)
-
- # Delay
- self.delay_lineEdit.setText(str(self.timer_time))
- self.delay_lineEdit.returnPressed.connect(self.delayChanged)
- self.delay_lineEdit.textEdited.connect(lambda: setLineEditBackground(
- self.delay_lineEdit, 'edited', self._font_size))
- setLineEditBackground(self.delay_lineEdit, 'synced', self._font_size)
- else:
- self.frame_device.hide()
+ self.axisChanged)
+
+ # Variable frame
+ self.variable_lineEdit = MyLineEdit()
+ self.variable_lineEdit.skip_has_eval = True
+ self.variable_lineEdit.use_np_pd = False
+ self.variable_lineEdit.setToolTip('Variable address e.g. ct400.scan.data')
+ self.layout_variable.addWidget(self.variable_lineEdit, 1, 2)
+ self.variable_lineEdit.setText(f'{self.dataManager.variable_address}')
+ self.variable_lineEdit.returnPressed.connect(self.variableChanged)
+ self.variable_lineEdit.textEdited.connect(lambda: setLineEditBackground(
+ self.variable_lineEdit, 'edited', self._font_size))
+ setLineEditBackground(self.variable_lineEdit, 'synced', self._font_size)
+
+ # Plot button
+ self.plotDataButton.clicked.connect(lambda state: self.refreshPlotData())
+
+ # Timer
+ self.timer_time = 0.5 # This plotter is not meant for fast plotting like the monitor, be aware it may crash with too high refreshing rate
+ self.timer = QtCore.QTimer(self)
+ self.timer.setInterval(int(self.timer_time*1000)) # ms
+ self.timer.timeout.connect(self.autoRefreshPlotData)
+
+ self.auto_plotDataButton.clicked.connect(self.autoRefreshChanged)
+
+ # Delay
+ self.delay_lineEdit.setText(str(self.timer_time))
+ self.delay_lineEdit.returnPressed.connect(self.delayChanged)
+ self.delay_lineEdit.textEdited.connect(lambda: setLineEditBackground(
+ self.delay_lineEdit, 'edited', self._font_size))
+ setLineEditBackground(self.delay_lineEdit, 'synced', self._font_size)
+ # / Variable frame
self.overwriteDataButton.clicked.connect(self.overwriteDataChanged)
@@ -150,10 +187,26 @@ def __init__(self, mainGui):
self.timerQueue = QtCore.QTimer(self)
self.timerQueue.setInterval(int(50)) # ms
self.timerQueue.timeout.connect(self._queueDriverHandler)
- self.timerQueue.start() # OPTIMIZE: should be started only when needed but difficult to know it before openning device which occurs in a diff thread! (can't start timer on diff thread)
+ self._stop_timerQueue = False
self.processPlugin()
+ self.splitter.setSizes([600, 100]) # height
+ self.splitter_2.setSizes([310, 310, 310]) # width
+
+ for splitter in (self.splitter, self.splitter_2, self.splitter_3):
+ for i in range(splitter.count()):
+ handle = splitter.handle(i)
+ handle.setStyleSheet("background-color: #DDDDDD;")
+ handle.installEventFilter(self)
+
+ def eventFilter(self, obj, event):
+ if event.type() == QtCore.QEvent.Enter:
+ obj.setStyleSheet("background-color: #AAAAAA;") # Hover color
+ elif event.type() == QtCore.QEvent.Leave:
+ obj.setStyleSheet("background-color: #DDDDDD;") # Normal color
+ return super().eventFilter(obj, event)
+
def createWidget(self, widget: Type, *args, **kwargs):
""" Function used by a driver to add a widget.
Mainly used to open a figure outside the GUI from a driver. """
@@ -196,19 +249,25 @@ def _queueDriverHandler(self):
if widget is not None:
widget_pos = list(d.values()).index(widget)
if widget_pos is not None:
- widget_name = list(d.keys())[widget_pos]
+ widget_name = list(d)[widget_pos]
widget = d.get(widget_name)
if widget is not None:
widget = d.pop(widget_name)
try: self.figureManager.ax.removeItem(widget)
- except Exception as e: self.setStatus(str(e), 10000, False)
+ except Exception as e:
+ self.setStatus(str(e), 10000, False)
+
+ if self._stop_timerQueue:
+ self.timerQueue.stop()
+ self._stop_timerQueue = False
def timerAction(self):
- """ This function checks if a module has been loaded and put to the queue. If so, associate item and module """
+ """ This function checks if a module has been loaded and put to the queue.
+ If so, associate item and module """
threadItemDictTemp = self.threadItemDict.copy()
threadDeviceDictTemp = self.threadDeviceDict.copy()
- for item_id in threadDeviceDictTemp.keys():
+ for item_id in threadDeviceDictTemp:
item = threadItemDictTemp[item_id]
module = threadDeviceDictTemp[item_id]
@@ -225,10 +284,13 @@ def timerAction(self):
def itemClicked(self, item):
""" Function called when a normal click has been detected in the tree.
Check the association if it is a main item """
- if item.parent() is None and item.loaded is False and id(item) not in self.threadItemDict.keys():
+ if (item.parent() is None
+ and item.loaded is False
+ and id(item) not in self.threadItemDict):
self.threadItemDict[id(item)] = item # needed before start of timer to avoid bad timing and to stop thread before loading is done
self.threadManager.start(item, 'load') # load device and add it to queue for timer to associate it later (doesn't block gui while device is openning)
self.timerPlugin.start()
+ self.timerQueue.start()
def rightClick(self, position):
""" Function called when a right click has been detected in the tree """
@@ -236,27 +298,42 @@ def rightClick(self, position):
if hasattr(item,'menu'):
item.menu(position)
- def processPlugin(self):
+ def hide_plugin_frame(self):
+ sizes = self.splitter_3.sizes()
+ self.splitter_3.setSizes([0, sizes[0]])
+ def processPlugin(self):
# Create frame
- self.frame = QtWidgets.QFrame()
- self.splitter_2.insertWidget(1, self.frame)
- self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
- layout = QtWidgets.QVBoxLayout(self.frame)
+ frame = QtWidgets.QFrame()
+ self.splitter_3.insertWidget(0, frame)
+ frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
+ layout = QtWidgets.QVBoxLayout(frame)
+
+ frame2 = QtWidgets.QFrame()
+ layout2 = QtWidgets.QHBoxLayout(frame2)
+ layout2.setContentsMargins(0,0,0,0)
- label = QtWidgets.QLabel('Plugin:', self.frame)
+ label = QtWidgets.QLabel('Plugin:', frame)
label.setToolTip("Drag and drop a device from the control panel to add a plugin to the plugin tree")
- layout.addWidget(label)
+ layout2.addWidget(label)
font = QtGui.QFont()
font.setBold(True)
label.setFont(font)
+ hide_plugin_button = QtWidgets.QPushButton('-')
+ hide_plugin_button.setMaximumSize(40, 23)
+ hide_plugin_button.clicked.connect(self.hide_plugin_frame)
+ layout2.addWidget(hide_plugin_button)
+
+ layout.addWidget(frame2)
+
# Tree widget configuration
- self.tree = MyQTreeWidget(self.frame, self)
+ self.tree = MyQTreeWidget(self, frame)
layout.addWidget(self.tree)
- self.tree.setHeaderLabels(['Plugin','Type','Actions','Values',''])
+ self.tree.setHeaderLabels(['Plugin', 'Type', 'Actions', 'Values', ''])
self.tree.header().setDefaultAlignment(QtCore.Qt.AlignCenter)
- self.tree.header().resizeSection(0, 170)
+ self.tree.header().setMinimumSectionSize(15)
+ self.tree.header().resizeSection(0, 160)
self.tree.header().hideSection(1)
self.tree.header().resizeSection(2, 50)
self.tree.header().resizeSection(3, 70)
@@ -268,31 +345,26 @@ def processPlugin(self):
self.tree.itemClicked.connect(self.itemClicked)
self.tree.customContextMenuRequested.connect(self.rightClick)
- plotter_config = config.load_config("plotter")
+ plotter_config = load_config("plotter_config")
- if 'plugin' in plotter_config.sections() and len(plotter_config['plugin']) != 0:
- self.splitter_2.setSizes([200,300,80,80])
- for plugin_nickname in plotter_config['plugin'].keys() :
- plugin_name = plotter_config['plugin'][plugin_nickname]
- self.addPlugin(plugin_name, plugin_nickname)
- else:
- self.splitter.setSizes([400,40])
- self.splitter_2.setSizes([200,80,80,80])
+ self.splitter_3.setSizes([280, 800])
- def addPlugin(self, plugin_name, plugin_nickname=None):
+ if ('plugin' in plotter_config.sections()
+ and len(plotter_config['plugin']) != 0):
+ for plugin_nickname, plugin_name in plotter_config['plugin'].items():
+ self.addPlugin(plugin_name, plugin_nickname)
- if plugin_nickname is None:
- plugin_nickname = plugin_name
+ def addPlugin(self, plugin_name: str, plugin_nickname: str):
- if plugin_name in devices.list_devices():
+ if plugin_name in list_devices():
plugin_nickname = self.getUniqueName(plugin_nickname)
self.all_plugin_list.append(plugin_nickname)
item = TreeWidgetItemModule(self.tree,plugin_name,plugin_nickname,self)
item.setBackground(0, QtGui.QColor('#9EB7F5')) # blue
-
- self.itemClicked(item)
else:
- self.setStatus(f"Error: plugin {plugin_name} not found in devices_config.ini",10000, False)
+ self.setStatus(
+ f"Error: plugin {plugin_name} not found in devices_config.ini",
+ 10000, False)
def associate(self, item, module):
@@ -306,16 +378,20 @@ def associate(self, item, module):
try:
variable()
except:
- self.setStatus(f"Can't read variable {variable.address()} on instantiation", 10000, False)
+ self.setStatus(
+ f"Can't read variable {variable.address()} on instantiation",
+ 10000, False)
try:
data = self.dataManager.getLastSelectedDataset().data
- data = data[[self.figureManager.getLabel("x"),self.figureManager.getLabel("y")]].copy()
+ data = data[[self.variable_x_comboBox.currentText(),
+ self.variable_y_comboBox.currentText()]].copy()
module.instance.refresh(data)
except Exception:
pass
def getUniqueName(self, basename: str):
- """ This function adds a number next to basename in case this basename is already taken """
+ """ This function adds a number next to basename in case this basename
+ is already taken """
names = self.all_plugin_list
name = basename
@@ -340,7 +416,9 @@ def dropEvent(self, event):
def dragEnterEvent(self, event):
""" Check that drop filenames """
# only accept if there is at least one filename in the dropped filenames -> refuse folders
- if event.mimeData().hasUrls() and any([os.path.isfile(e.toLocalFile()) for e in event.mimeData().urls()]):
+ if (event.mimeData().hasUrls()
+ and any([os.path.isfile(e.toLocalFile())
+ for e in event.mimeData().urls()])):
event.accept()
qwidget_children = self.findChildren(QtWidgets.QWidget)
@@ -360,9 +438,15 @@ def dragLeaveEvent(self, event):
def plugin_refresh(self):
if self.active_plugin_dict:
self.clearStatus()
- if hasattr(self.dataManager.getLastSelectedDataset(),"data"):
- data = self.dataManager.getLastSelectedDataset().data
- data = data[[self.figureManager.getLabel("x"),self.figureManager.getLabel("y")]].copy()
+ dataset = self.dataManager.getLastSelectedDataset()
+ if hasattr(dataset, "data"):
+ data = dataset.data
+ variable_x = self.variable_x_comboBox.currentText()
+ variable_y = self.variable_y_comboBox.currentText()
+ if (variable_x in data.columns and variable_y in data.columns):
+ data = data[[variable_x, variable_y]].copy()
+ else:
+ data = None
else:
data = None
@@ -370,8 +454,9 @@ def plugin_refresh(self):
if hasattr(module.instance, "refresh"):
try:
module.instance.refresh(data)
- except Exception as error:
- self.setStatus(f"Error in plugin {module.name}: '{error}'",10000, False)
+ except Exception as e:
+ self.setStatus(f"Error in plugin {module.name}: {e}",
+ 10000, False)
def overwriteDataChanged(self):
""" Set overwrite name for data import """
@@ -389,28 +474,27 @@ def autoRefreshPlotData(self):
# OPTIMIZE: timer should not call a heavy function, idealy just take data to plot
self.refreshPlotData()
- def refreshPlotData(self):
+ def refreshPlotData(self, variable: Union[Variable, Variable_og, pd.DataFrame, Any] = None):
""" This function get the last dataset data and display it onto the Plotter GUI """
- deviceValue = self.dataManager.getDeviceValue()
-
try:
- deviceVariable = self.dataManager.getDeviceName(deviceValue)
- dataset = self.dataManager.importDeviceData(deviceVariable)
- data_name = dataset.name
+ if variable is None:
+ variable_address = self.dataManager.get_variable_address()
+ variable = self.dataManager.getVariable(variable_address)
+ dataset = self.dataManager.importDeviceData(variable)
self.figureManager.start(dataset)
- self.setStatus(f"Display the data: '{data_name}'", 5000)
- except Exception as error:
- self.setStatus(f"Can't refresh data: {error}", 10000, False)
+ self.setStatus(f"Display the data: '{dataset.name}'", 5000)
+ except Exception as e:
+ self.setStatus(f"Can't refresh data: {e}", 10000, False)
- def deviceChanged(self):
+ def variableChanged(self):
""" This function start the update of the target value in the data manager
when a changed has been detected """
# Send the new value
try:
- value = str(self.device_lineEdit.text())
- self.dataManager.setDeviceValue(value)
- except Exception as er:
- self.setStatus(f"ERROR Can't change device variable: {er}", 10000, False)
+ value = str(self.variable_lineEdit.text())
+ self.dataManager.set_variable_address(value)
+ except Exception as e:
+ self.setStatus(f"Can't change variable name: {e}", 10000, False)
else:
# Rewrite the GUI with the current value
self.updateDeviceValueGui()
@@ -418,16 +502,17 @@ def deviceChanged(self):
def updateDeviceValueGui(self):
""" This function ask the current value of the target value in the data
manager, and then update the GUI """
- value = self.dataManager.getDeviceValue()
- self.device_lineEdit.setText(f'{value}')
- setLineEditBackground(self.device_lineEdit, 'synced', self._font_size)
+ value = self.dataManager.get_variable_address()
+ self.variable_lineEdit.setText(f'{value}')
+ setLineEditBackground(self.variable_lineEdit, 'synced', self._font_size)
- def variableChanged(self,index):
+ def axisChanged(self, index):
""" This function is called when the displayed result has been changed in
the combo box. It proceeds to the change. """
self.figureManager.clearData()
- if self.variable_x_comboBox.currentIndex() != -1 and self.variable_y_comboBox.currentIndex() != -1 :
+ if (self.variable_x_comboBox.currentIndex() != -1
+ and self.variable_y_comboBox.currentIndex() != -1):
self.figureManager.reloadData()
def nbTracesChanged(self):
@@ -450,18 +535,20 @@ def nbTracesChanged(self):
if check is True and self.variable_y_comboBox.currentIndex() != -1:
self.figureManager.reloadData()
- def closeEvent(self,event):
+ def closeEvent(self, event):
""" This function does some steps before the window is closed (not killed) """
if hasattr(self, 'timer'): self.timer.stop()
self.timerPlugin.stop()
self.timerQueue.stop()
- if hasattr(self.mainGui, 'clearPlotter'):
- self.mainGui.clearPlotter()
+ clearPlotter()
+
+ if not self.has_parent:
+ closePlotter()
super().closeEvent(event)
- if self.mainGui is None:
+ if not self.has_parent:
QtWidgets.QApplication.quit() # close the plotter app
def close(self):
@@ -494,7 +581,7 @@ def updateDelayGui(self):
self.timer.setInterval(int(value*1000)) # ms
setLineEditBackground(self.delay_lineEdit, 'synced', self._font_size)
- def setStatus(self,message, timeout=0, stdout=True):
+ def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
""" Modify the message displayed in the status bar and add error message to logger """
self.statusBar.showMessage(message, timeout)
if not stdout: print(message, file=sys.stderr)
diff --git a/autolab/core/gui/plotting/thread.py b/autolab/core/gui/plotting/thread.py
index 93a0fed2..a4f6eaea 100644
--- a/autolab/core/gui/plotting/thread.py
+++ b/autolab/core/gui/plotting/thread.py
@@ -5,32 +5,28 @@
@author: qchat
"""
+import sys
import inspect
+from typing import Any
-from qtpy import QtCore
+from qtpy import QtCore, QtWidgets
-from ... import devices
-from ... import drivers
from ..GUI_utilities import qt_object_exists
+from ...devices import get_final_device_config, Device
+from ...drivers import load_driver_lib, get_driver
-class ThreadManager :
-
+class ThreadManager:
""" This class is dedicated to manage the different threads,
from their creation, to their deletion after they have been used """
-
- def __init__(self,gui):
+ def __init__(self, gui: QtWidgets.QMainWindow):
self.gui = gui
self.threads = {}
-
-
- def start(self,item,intType,value=None):
-
+ def start(self, item: QtWidgets.QTreeWidgetItem, intType: str, value = None):
""" This function is called when a new thread is requested,
for a particular intType interaction type """
-
# GUI disabling
item.setDisabled(True)
@@ -42,114 +38,107 @@ def start(self,item,intType,value=None):
item.valueWidget.setEnabled(False)
# disabling valueWidget deselect item and select next one, need to disable all items and reenable item
- list_item = self.gui.tree.selectedItems()
- for item_selected in list_item:
+ for item_selected in self.gui.tree.selectedItems():
item_selected.setSelected(False)
item.setSelected(True)
# Status writing
- if intType == 'read' : status = f'Reading {item.variable.address()}...'
- elif intType == 'write' : status = f'Writing {item.variable.address()}...'
- elif intType == 'execute' : status = f'Executing {item.action.address()}...'
- elif intType == 'load' : status = f'Loading plugin {item.name}...'
+ if intType == 'read': status = f'Reading {item.variable.address()}...'
+ elif intType == 'write': status = f'Writing {item.variable.address()}...'
+ elif intType == 'execute': status = f'Executing {item.action.address()}...'
+ elif intType == 'load': status = f'Loading plugin {item.name}...'
self.gui.setStatus(status)
# Thread configuration
thread = InteractionThread(item,intType,value)
tid = id(thread)
self.threads[tid] = thread
- thread.endSignal.connect(lambda error, x=tid : self.threadFinished(x,error))
- thread.finished.connect(lambda x=tid : self.delete(x))
+ thread.endSignal.connect(lambda error, x=tid: self.threadFinished(x, error))
+ thread.finished.connect(lambda x=tid: self.delete(x))
# Starting thread
thread.start()
-
- def threadFinished(self,tid,error):
-
+ def threadFinished(self, tid: int, error: Exception):
""" This function is called when a thread has finished its job, with an error or not
It updates the status bar of the GUI in consequence and enabled back the correspondig item """
-
- if error is None : self.gui.clearStatus()
- else : self.gui.setStatus(str(error), 10000, False)
+ if error:
+ if qt_object_exists(self.gui.statusBar):
+ self.gui.setStatus(str(error), 10000, False)
+ else:
+ print(str(error), file=sys.stderr)
+ else:
+ if qt_object_exists(self.gui.statusBar):
+ self.gui.clearStatus()
item = self.threads[tid].item
item.setDisabled(False)
- if hasattr(item, "execButton"):
- if qt_object_exists(item.execButton):
- item.execButton.setEnabled(True)
- if hasattr(item, "readButton"):
- if qt_object_exists(item.readButton):
- item.readButton.setEnabled(True)
- if hasattr(item, "valueWidget"):
- if qt_object_exists(item.valueWidget):
- item.valueWidget.setEnabled(True)
-
-
- def delete(self,tid):
-
+ if hasattr(item, "execButton") and qt_object_exists(item.execButton):
+ item.execButton.setEnabled(True)
+ if hasattr(item, "readButton") and qt_object_exists(item.readButton):
+ item.readButton.setEnabled(True)
+ if hasattr(item, "valueWidget") and qt_object_exists(item.valueWidget):
+ item.valueWidget.setEnabled(True)
+ # Put back focus if item still selected (item.isSelected() doesn't work)
+ if item in self.gui.tree.selectedItems():
+ item.valueWidget.setFocus()
+
+ def delete(self, tid: int):
""" This function is called when a thread is about to be deleted.
This removes it from the dictionnary self.threads, for a complete deletion """
-
self.threads.pop(tid)
-
-
-
-
class InteractionThread(QtCore.QThread):
-
""" This class is dedicated to operation interaction with the devices, in a new thread """
-
endSignal = QtCore.Signal(object)
- def __init__(self, item, intType, value):
+ def __init__(self, item: QtWidgets.QTreeWidgetItem, intType: str, value: Any):
super().__init__()
self.item = item
self.intType = intType
self.value = value
def run(self):
-
- """ Depending on the interaction type requested, this function reads or writes a variable,
- or execute an action. """
-
+ """ Depending on the interaction type requested, this function reads or
+ writes a variable, or execute an action. """
error = None
-
- try :
- if self.intType == 'read' : self.item.variable()
- elif self.intType == 'write' :
+ try:
+ if self.intType == 'read': self.item.variable()
+ elif self.intType == 'write':
self.item.variable(self.value)
- if self.item.variable.readable : self.item.variable()
- elif self.intType == 'execute' :
- if self.value is not None :
+ if self.item.variable.readable: self.item.variable()
+ elif self.intType == 'execute':
+ if self.value is not None:
self.item.action(self.value)
- else :
+ else:
self.item.action()
- elif self.intType == 'load' :
+ elif self.intType == 'load':
# Note that threadItemDict needs to be updated outside of thread to avoid timing error
plugin_name = self.item.name
- device_config = devices.get_final_device_config(plugin_name)
+ device_config = get_final_device_config(plugin_name)
- driver_kwargs = { k:v for k,v in device_config.items() if k not in ['driver','connection']}
- driver_lib = drivers.load_driver_lib(device_config['driver'])
+ driver_kwargs = {k: v for k, v in device_config.items()
+ if k not in ['driver', 'connection']}
+ driver_lib = load_driver_lib(device_config['driver'])
- if hasattr(driver_lib, 'Driver') and 'gui' in [param.name for param in inspect.signature(driver_lib.Driver.__init__).parameters.values()]:
+ if hasattr(driver_lib, 'Driver') and 'gui' in [
+ param.name for param in inspect.signature(
+ driver_lib.Driver.__init__).parameters.values()]:
driver_kwargs['gui'] = self.item.gui
- instance = drivers.get_driver(device_config['driver'],
- device_config['connection'],
- **driver_kwargs)
- module = devices.Device(plugin_name, instance, device_config)
+ instance = get_driver(
+ device_config['driver'], device_config['connection'],
+ **driver_kwargs)
+ module = Device(plugin_name, instance, device_config)
self.item.gui.threadDeviceDict[id(self.item)] = module
except Exception as e:
error = e
- if self.intType == 'load' :
- error = f'An error occured when loading device {self.item.name} : {str(e)}'
+ if self.intType == 'load':
+ error = f'An error occured when loading device {self.item.name}: {str(e)}'
if id(self.item) in self.item.gui.threadItemDict.keys():
self.item.gui.threadItemDict.pop(id(self.item))
self.endSignal.emit(error)
diff --git a/autolab/core/gui/plotting/treewidgets.py b/autolab/core/gui/plotting/treewidgets.py
index 6e4dc453..01f07235 100644
--- a/autolab/core/gui/plotting/treewidgets.py
+++ b/autolab/core/gui/plotting/treewidgets.py
@@ -5,34 +5,40 @@
@author: qchat
"""
-
+from typing import Any, Union
import os
import pandas as pd
import numpy as np
+from qtpy import QtCore, QtWidgets, QtGui
-from qtpy import QtCore, QtWidgets
-
-from .. import variables
-from ..GUI_utilities import qt_object_exists
-from ... import paths, config
-from ...utilities import SUPPORTED_EXTENSION
+from ..icons import icons
+from ..GUI_utilities import (MyLineEdit, MyInputDialog, MyQCheckBox, MyQComboBox,
+ qt_object_exists)
+from ...paths import PATHS
+from ...config import get_control_center_config
+from ...variables import eval_variable, has_eval
+from ...utilities import (SUPPORTED_EXTENSION, str_to_array, array_to_str,
+ dataframe_to_str, str_to_dataframe, create_array,
+ str_to_tuple)
+# OPTIMIZE: Could merge treewidgets from control panel and plotter (or common version & subclass)
class TreeWidgetItemModule(QtWidgets.QTreeWidgetItem):
""" This class represents a module in an item of the tree """
def __init__(self, itemParent, name, nickname, gui):
- super().__init__(itemParent, [nickname, 'Module'])
- self.setTextAlignment(1, QtCore.Qt.AlignHCenter)
self.name = name
- self.nickname = nickname
self.module = None
self.loaded = False
self.gui = gui
-
self.is_not_submodule = isinstance(gui.tree, type(itemParent))
+ self.nickname = nickname
+
+ super().__init__(itemParent, [nickname, 'Module'])
+
+ self.setTextAlignment(1, QtCore.Qt.AlignHCenter)
def load(self, module):
""" This function loads the entire module (submodules, variables, actions) """
@@ -41,15 +47,15 @@ def load(self, module):
# Submodules
subModuleNames = self.module.list_modules()
for subModuleName in subModuleNames:
- subModule = getattr(self.module,subModuleName)
- item = TreeWidgetItemModule(self, subModuleName,subModuleName,self.gui)
+ subModule = getattr(self.module, subModuleName)
+ item = TreeWidgetItemModule(self, subModuleName, subModuleName, self.gui)
item.load(subModule)
# Variables
varNames = self.module.list_variables()
for varName in varNames:
variable = getattr(self.module,varName)
- TreeWidgetItemVariable(self, variable,self.gui)
+ TreeWidgetItemVariable(self, variable, self.gui)
# Actions
actNames = self.module.list_actions()
@@ -81,6 +87,9 @@ def menu(self, position):
self.removeChild(self.child(0))
self.loaded = False
+ if not self.gui.active_plugin_dict:
+ self.gui._stop_timerQueue = True
+
class TreeWidgetItemAction(QtWidgets.QTreeWidgetItem):
""" This class represents an action in an item of the tree """
@@ -97,8 +106,12 @@ def __init__(self, itemParent, action, gui):
self.gui = gui
self.action = action
+ # Import Autolab config
+ control_center_config = get_control_center_config()
+ self.precision = int(float(control_center_config['precision']))
+
if self.action.has_parameter:
- if self.action.type in [int, float, str, pd.DataFrame, np.ndarray]:
+ if self.action.type in [int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]:
self.executable = True
self.has_value = True
else:
@@ -117,77 +130,229 @@ def __init__(self, itemParent, action, gui):
# Main - Column 3 : QlineEdit if the action has a parameter
if self.has_value:
- self.valueWidget = QtWidgets.QLineEdit()
- self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
- self.gui.tree.setItemWidget(self, 3, self.valueWidget)
- self.valueWidget.returnPressed.connect(self.execute)
+ if self.action.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
+ self.valueWidget = MyLineEdit()
+ self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
+ self.gui.tree.setItemWidget(self, 3, self.valueWidget)
+ self.valueWidget.returnPressed.connect(self.execute)
+ self.valueWidget.textEdited.connect(self.valueEdited)
+
+ ## QCheckbox for boolean variables
+ elif self.action.type in [bool]:
+ self.valueWidget = MyQCheckBox(self)
+ hbox = QtWidgets.QHBoxLayout()
+ hbox.addWidget(self.valueWidget)
+ hbox.setAlignment(QtCore.Qt.AlignCenter)
+ hbox.setSpacing(0)
+ hbox.setContentsMargins(0,0,0,0)
+ widget = QtWidgets.QWidget()
+ widget.setLayout(hbox)
+
+ self.gui.tree.setItemWidget(self, 3, widget)
+
+ ## Combobox for tuples: Tuple[List[str], int]
+ elif self.action.type in [tuple]:
+ self.valueWidget = MyQComboBox()
+ self.valueWidget.wheel = False # prevent changing value by mistake
+ self.valueWidget.key = False
+ self.valueWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.valueWidget.customContextMenuRequested.connect(self.openInputDialog)
+
+ self.gui.tree.setItemWidget(self, 3, self.valueWidget)
+
+ # Main - column 4 : indicator (status of the actual value : known or not known)
+ self.indicator = QtWidgets.QLabel()
+ self.gui.tree.setItemWidget(self, 4, self.indicator)
# Tooltip
if self.action._help is None: tooltip = 'No help available for this action'
else: tooltip = self.action._help
- self.setToolTip(0,tooltip)
+ self.setToolTip(0, tooltip)
- def readGui(self):
- """ This function returns the value in good format of the value in the GUI """
- value = self.valueWidget.text()
-
- if value == '':
- if self.action.unit in ('open-file', 'save-file', 'filename'):
- if self.action.unit == "filename": # TODO: LEGACY (to remove later)
- self.gui.setStatus("Using 'filename' as unit is depreciated in favor of 'open-file' and 'save-file'" \
- f"\nUpdate driver {self.action.name} to remove this warning",
- 10000, False)
- self.action.unit = "open-file"
-
- if self.action.unit == "open-file":
- filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.gui, caption=f"Open file - {self.action.name}",
- directory=paths.USER_LAST_CUSTOM_FOLDER,
- filter=SUPPORTED_EXTENSION)
- elif self.action.unit == "save-file":
- filename, _ = QtWidgets.QFileDialog.getSaveFileName(
- self.gui, caption=f"Save file - {self.action.name}",
- directory=paths.USER_LAST_CUSTOM_FOLDER,
- filter=SUPPORTED_EXTENSION)
-
- if filename != '':
- path = os.path.dirname(filename)
- paths.USER_LAST_CUSTOM_FOLDER = path
- return filename
- else:
+ self.writeSignal = WriteSignal()
+ self.writeSignal.writed.connect(self.valueWrited)
+ self.action._write_signal = self.writeSignal
+
+ def openInputDialog(self, position: QtCore.QPoint):
+ """ Only used for tuple """
+ menu = QtWidgets.QMenu()
+ modifyTuple = menu.addAction("Modify tuple")
+ modifyTuple.setIcon(icons['tuple'])
+
+ choice = menu.exec_(self.valueWidget.mapToGlobal(position))
+
+ if choice == modifyTuple:
+ main_dialog = MyInputDialog(self.gui, self.action.address())
+ main_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ if self.action.type in [tuple]:
+ main_dialog.setTextValue(str(self.action.value))
+ main_dialog.show()
+
+ if main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
+ response = main_dialog.textValue()
+ else:
+ response = ''
+
+ if qt_object_exists(main_dialog): main_dialog.deleteLater()
+
+ if response != '':
+ try:
+ if has_eval(response):
+ response = eval_variable(response)
+ if self.action.type in [tuple]:
+ response = str_to_tuple(str(response))
+ except Exception as e:
self.gui.setStatus(
- f"Action {self.action.name} cancel filename selection",
- 10000)
- elif self.action.unit == "user-input":
- response, _ = QtWidgets.QInputDialog.getText(
- self.gui, self.action.name, f"Set {self.action.name} value",
- QtWidgets.QLineEdit.Normal)
-
- if response != '':
- return response
+ f"Variable {self.action.address()}: {e}", 10000, False)
+ return None
+
+ self.action.value = response
+ self.valueWrited(response)
+ self.valueEdited()
+
+ def writeGui(self, value):
+ """ This function displays a new value in the GUI """
+ if hasattr(self, 'valueWidget') and qt_object_exists(self.valueWidget): # avoid crash if device closed and try to write gui (if close device before reading finished)
+ # Update value
+ if self.action.type in [int, float]:
+ self.valueWidget.setText(f'{value:.{self.precision}g}') # default is .6g
+ elif self.action.type in [str]:
+ self.valueWidget.setText(value)
+ elif self.action.type in [bytes]:
+ self.valueWidget.setText(value.decode())
+ elif self.action.type in [bool]:
+ self.valueWidget.setChecked(value)
+ elif self.action.type in [tuple]:
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
+ if value[0] != items:
+ self.valueWidget.clear()
+ self.valueWidget.addItems(value[0])
+ self.valueWidget.setCurrentIndex(value[1])
+ elif self.action.type in [np.ndarray]:
+ self.valueWidget.setText(array_to_str(value))
+ elif self.action.type in [pd.DataFrame]:
+ self.valueWidget.setText(dataframe_to_str(value))
+ else:
+ self.valueWidget.setText(value)
+
+ def readGui(self) -> Any:
+ """ This function returns the value in good format of the value in the GUI """
+ if self.action.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
+ value = self.valueWidget.text()
+
+ if value == '':
+ if self.action.unit in ('open-file', 'save-file', 'filename'):
+ if self.action.unit == "filename": # TODO: LEGACY (to remove later)
+ self.gui.setStatus("Using 'filename' as unit is depreciated in favor of 'open-file' and 'save-file'" \
+ f"\nUpdate driver '{self.action.address().split('.')[0]}' to remove this warning",
+ 10000, False)
+ self.action.unit = "open-file"
+
+ if self.action.unit == "open-file":
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.gui, caption=f"Open file - {self.action.address()}",
+ directory=PATHS['last_folder'],
+ filter=SUPPORTED_EXTENSION)
+ elif self.action.unit == "save-file":
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
+ self.gui, caption=f"Save file - {self.action.address()}",
+ directory=PATHS['last_folder'],
+ filter=SUPPORTED_EXTENSION)
+
+ if filename != '':
+ path = os.path.dirname(filename)
+ PATHS['last_folder'] = path
+ return filename
+ else:
+ self.gui.setStatus(
+ f"Action {self.action.address()} cancel filename selection",
+ 10000)
+ elif self.action.unit == "user-input":
+ main_dialog = MyInputDialog(self.gui, self.action.address())
+ main_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ main_dialog.show()
+
+ if main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
+ response = main_dialog.textValue()
+ else:
+ response = ''
+
+ if qt_object_exists(main_dialog): main_dialog.deleteLater()
+
+ if response != '':
+ return response
+ else:
+ self.gui.setStatus(
+ f"Action {self.action.address()} cancel user input",
+ 10000)
else:
self.gui.setStatus(
- f"Action {self.action.name} cancel user input",
- 10000)
+ f"Action {self.action.address()} requires a value for its parameter",
+ 10000, False)
else:
- self.gui.setStatus(
- f"Action {self.action.name} requires a value for its parameter",
- 10000, False)
+ try:
+ value = eval_variable(value)
+ if self.action.type in [int]:
+ value = int(float(value))
+ if self.action.type in [bytes]:
+ value = value.encode()
+ elif self.action.type in [np.ndarray]:
+ value = str_to_array(value) if isinstance(
+ value, str) else create_array(value)
+ elif self.action.type in [pd.DataFrame]:
+ if isinstance(value, str):
+ value = str_to_dataframe(value)
+ else:
+ value = self.action.type(value)
+ return value
+ except Exception as e:
+ self.gui.setStatus(
+ f"Action {self.action.address()}: {e}",
+ 10000, False)
+ elif self.action.type in [bool]:
+ value = self.valueWidget.isChecked()
+ return value
+ elif self.action.type in [tuple]:
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
+ value = (items, self.valueWidget.currentIndex())
+ return value
+
+ def setValueKnownState(self, state: Union[bool, float]):
+ """ Turn the color of the indicator depending of the known state of the value """
+ if state == 0.5:
+ self.indicator.setStyleSheet("background-color:#FFFF00") # yellow
+ self.indicator.setToolTip('Value written but not read')
+ elif state:
+ self.indicator.setStyleSheet("background-color:#70db70") # green
+ self.indicator.setToolTip('Value read')
else:
- try:
- value = variables.eval_variable(value)
- value = self.action.type(value)
- return value
- except:
- self.gui.setStatus(f"Action {self.action.name}: Impossible to convert {value} in type {self.action.type.__name__}",10000, False)
+ self.indicator.setStyleSheet("background-color:#ff8c1a") # orange
+ self.indicator.setToolTip('Value not up-to-date')
def execute(self):
""" Start a new thread to execute the associated action """
- if self.has_value:
- value = self.readGui()
- if value is not None: self.gui.threadManager.start(self, 'execute', value=value)
- else:
- self.gui.threadManager.start(self, 'execute')
+ if not self.isDisabled():
+ if self.has_value:
+ value = self.readGui()
+ if value is not None:
+ self.gui.threadManager.start(self, 'execute', value=value)
+ else:
+ self.gui.threadManager.start(self, 'execute')
+
+ def valueEdited(self):
+ """ Change indicator state when editing action parameter """
+ self.setValueKnownState(False)
+
+ def valueWrited(self, value: Any):
+ """ Called when action parameter written """
+ try:
+ if self.has_value:
+ self.writeGui(value)
+ self.setValueKnownState(0.5)
+ except Exception as e:
+ self.gui.setStatus(f"SHOULD NOT RAISE ERROR: {e}", 10000, False)
class TreeWidgetItemVariable(QtWidgets.QTreeWidgetItem):
@@ -203,44 +368,66 @@ def __init__(self, itemParent, variable, gui):
self.setTextAlignment(1, QtCore.Qt.AlignHCenter)
self.gui = gui
-
self.variable = variable
# Import Autolab config
- control_center_config = config.get_control_center_config()
- self.precision = int(control_center_config['precision'])
+ control_center_config = get_control_center_config()
+ self.precision = int(float(control_center_config['precision']))
# Signal creation and associations in autolab devices instances
self.readSignal = ReadSignal()
self.readSignal.read.connect(self.writeGui)
self.variable._read_signal = self.readSignal
self.writeSignal = WriteSignal()
- self.writeSignal.writed.connect(self.valueEdited)
+ self.writeSignal.writed.connect(self.valueWrited)
self.variable._write_signal = self.writeSignal
# Main - Column 2 : Creation of a READ button if the variable is readable
- if self.variable.readable and self.variable.type in [int, float, bool, str]:
+ if self.variable.readable and self.variable.type in [
+ int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]:
self.readButton = QtWidgets.QPushButton()
self.readButton.setText("Read")
self.readButton.clicked.connect(self.read)
- self.gui.tree.setItemWidget(self, 2, self.readButton)
+
+ if not self.variable.writable and self.variable.type in [
+ np.ndarray, pd.DataFrame]:
+ self.readButtonCheck = QtWidgets.QCheckBox()
+ self.readButtonCheck.stateChanged.connect(
+ self.readButtonCheckEdited)
+ self.readButtonCheck.setToolTip(
+ 'Toggle reading in text, ' \
+ 'careful can truncate data and impact performance')
+ self.readButtonCheck.setMaximumWidth(15)
+
+ frameReadButton = QtWidgets.QFrame()
+ hbox = QtWidgets.QHBoxLayout(frameReadButton)
+ hbox.setSpacing(0)
+ hbox.setContentsMargins(0,0,0,0)
+ hbox.addWidget(self.readButtonCheck)
+ hbox.addWidget(self.readButton)
+ self.gui.tree.setItemWidget(self, 2, frameReadButton)
+ else:
+ self.gui.tree.setItemWidget(self, 2, self.readButton)
# Main - column 3 : Creation of a VALUE widget, depending on the type
## QLineEdit or QLabel
- if self.variable.type in [int, float, str, pd.DataFrame, np.ndarray]:
-
+ if self.variable.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
if self.variable.writable:
- self.valueWidget = QtWidgets.QLineEdit()
+ self.valueWidget = MyLineEdit()
self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
self.valueWidget.returnPressed.connect(self.write)
self.valueWidget.textEdited.connect(self.valueEdited)
- # self.valueWidget.setPlaceholderText(self.variable._help) # OPTIMIZE: Could be nice but take too much place. Maybe add it as option
- elif self.variable.readable and self.variable.type in [int, float, str]:
+ self.valueWidget.setMaxLength(10000000) # default is 32767, not enought for array and dataframe
+ # self.valueWidget.setPlaceholderText(self.variable._help) # Could be nice but take too much place. Maybe add it as option
+ elif self.variable.readable:
self.valueWidget = QtWidgets.QLineEdit()
+ self.valueWidget.setMaxLength(10000000)
self.valueWidget.setReadOnly(True)
- self.valueWidget.setStyleSheet(
- "QLineEdit {border : 1px solid #a4a4a4; background-color : #f4f4f4}")
+ palette = self.valueWidget.palette()
+ palette.setColor(QtGui.QPalette.Base,
+ palette.color(QtGui.QPalette.Base).darker(107))
+ self.valueWidget.setPalette(palette)
self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
else:
self.valueWidget = QtWidgets.QLabel()
@@ -250,22 +437,7 @@ def __init__(self, itemParent, variable, gui):
## QCheckbox for boolean variables
elif self.variable.type in [bool]:
-
- class MyQCheckBox(QtWidgets.QCheckBox):
-
- def __init__(self, parent):
- self.parent = parent
- super().__init__()
-
- def mouseReleaseEvent(self, event):
- super().mouseReleaseEvent(event)
- self.parent.valueEdited()
- self.parent.write()
-
self.valueWidget = MyQCheckBox(self)
- # self.valueWidget = QtWidgets.QCheckBox()
- # self.valueWidget.stateChanged.connect(self.valueEdited)
- # self.valueWidget.stateChanged.connect(self.write) # removed this to avoid setting a second time when reading a change
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(self.valueWidget)
hbox.setAlignment(QtCore.Qt.AlignCenter)
@@ -273,14 +445,32 @@ def mouseReleaseEvent(self, event):
hbox.setContentsMargins(0,0,0,0)
widget = QtWidgets.QWidget()
widget.setLayout(hbox)
- if not self.variable.writable: # Disable interaction is not writable
+ if not self.variable.writable: # Disable interaction is not writable
self.valueWidget.setEnabled(False)
+
self.gui.tree.setItemWidget(self, 3, widget)
+ ## Combobox for tuples: Tuple[List[str], int]
+ elif self.variable.type in [tuple]:
+ if self.variable.writable:
+ self.valueWidget = MyQComboBox()
+ self.valueWidget.wheel = False # prevent changing value by mistake
+ self.valueWidget.key = False
+ self.valueWidget.activated.connect(self.write)
+ self.valueWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.valueWidget.customContextMenuRequested.connect(self.openInputDialog)
+ elif self.variable.readable:
+ self.valueWidget = MyQComboBox()
+ self.valueWidget.readonly = True
+ else:
+ self.valueWidget = QtWidgets.QLabel()
+ self.valueWidget.setAlignment(QtCore.Qt.AlignCenter)
+
+ self.gui.tree.setItemWidget(self, 3, self.valueWidget)
+
# Main - column 4 : indicator (status of the actual value : known or not known)
- if self.variable.type in [int, float, str, bool, np.ndarray, pd.DataFrame]:
- self.indicator = QtWidgets.QLabel()
- self.gui.tree.setItemWidget(self, 4, self.indicator)
+ self.indicator = QtWidgets.QLabel()
+ self.gui.tree.setItemWidget(self, 4, self.indicator)
# Tooltip
if self.variable._help is None: tooltip = 'No help available for this variable'
@@ -290,62 +480,161 @@ def mouseReleaseEvent(self, event):
tooltip += f" ({variable_type})"
self.setToolTip(0, tooltip)
+ def openInputDialog(self, position: QtCore.QPoint):
+ """ Only used for tuple """
+ menu = QtWidgets.QMenu()
+ modifyTuple = menu.addAction("Modify tuple")
+ modifyTuple.setIcon(icons['tuple'])
+
+ choice = menu.exec_(self.valueWidget.mapToGlobal(position))
+
+ if choice == modifyTuple:
+ main_dialog = MyInputDialog(self.gui, self.variable.address())
+ main_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ if self.variable.type in [tuple]:
+ main_dialog.setTextValue(str(self.variable.value))
+ main_dialog.show()
+
+ if main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
+ response = main_dialog.textValue()
+ else:
+ response = ''
+
+ if qt_object_exists(main_dialog): main_dialog.deleteLater()
+
+ if response != '':
+ try:
+ if has_eval(response):
+ response = eval_variable(response)
+ if self.variable.type in [tuple]:
+ response = str_to_tuple(str(response))
+ except Exception as e:
+ self.gui.setStatus(
+ f"Variable {self.variable.address()}: {e}", 10000, False)
+ return None
+
+ self.variable(response)
+
+ if self.variable.readable:
+ self.variable()
+
def writeGui(self, value):
""" This function displays a new value in the GUI """
- if hasattr(self, 'valueWidget') and qt_object_exists(self.valueWidget): # avoid crash if device closed and try to write gui (if close device before reading finihsed)
+ if hasattr(self, 'valueWidget') and qt_object_exists(self.valueWidget): # avoid crash if device closed and try to write gui (if close device before reading finished)
# Update value
if self.variable.numerical:
self.valueWidget.setText(f'{value:.{self.precision}g}') # default is .6g
elif self.variable.type in [str]:
self.valueWidget.setText(value)
+ elif self.variable.type in [bytes]:
+ self.valueWidget.setText(value.decode())
elif self.variable.type in [bool]:
self.valueWidget.setChecked(value)
+ elif self.variable.type in [tuple]:
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
+ if value[0] != items:
+ self.valueWidget.clear()
+ self.valueWidget.addItems(value[0])
+ self.valueWidget.setCurrentIndex(value[1])
+ elif self.variable.type in [np.ndarray, pd.DataFrame]:
+ if self.variable.writable or self.readButtonCheck.isChecked():
+ if self.variable.type in [np.ndarray]:
+ self.valueWidget.setText(array_to_str(value))
+ if self.variable.type in [pd.DataFrame]:
+ self.valueWidget.setText(dataframe_to_str(value))
+ # else:
+ # self.valueWidget.setText('')
# Change indicator light to green
- if self.variable.type in [int, float, bool, str, np.ndarray, pd.DataFrame]:
+ if self.variable.type in [
+ int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]:
self.setValueKnownState(True)
def readGui(self):
""" This function returns the value in good format of the value in the GUI """
- if self.variable.type in [int, float, str, np.ndarray, pd.DataFrame]:
+ if self.variable.type in [int, float, str, bytes, np.ndarray, pd.DataFrame]:
value = self.valueWidget.text()
if value == '':
self.gui.setStatus(
- f"Variable {self.variable.name} requires a value to be set",
+ f"Variable {self.variable.address()} requires a value to be set",
10000, False)
else:
try:
- value = variables.eval_variable(value)
- value = self.variable.type(value)
+ value = eval_variable(value)
+ if self.variable.type in [int]:
+ value = int(float(value))
+ if self.variable.type in [bytes]:
+ value = value.encode()
+ elif self.variable.type in [np.ndarray]:
+ if isinstance(value, str): value = str_to_array(value)
+ else: value = create_array(value)
+ elif self.variable.type in [pd.DataFrame]:
+ if isinstance(value, str): value = str_to_dataframe(value)
+ else:
+ value = self.variable.type(value)
return value
- except:
- self.gui.setStatus(f"Variable {self.variable.name}: Impossible to convert {value} in type {self.variable.type.__name__}",10000, False)
+ except Exception as e:
+ self.gui.setStatus(
+ f"Variable {self.variable.address()}: {e}",
+ 10000, False)
elif self.variable.type in [bool]:
value = self.valueWidget.isChecked()
return value
+ elif self.variable.type in [tuple]:
+ items = [self.valueWidget.itemText(i)
+ for i in range(self.valueWidget.count())]
+ value = (items, self.valueWidget.currentIndex())
+ return value
- def setValueKnownState(self, state):
+ def setValueKnownState(self, state: Union[bool, float]):
""" Turn the color of the indicator depending of the known state of the value """
- if state: self.indicator.setStyleSheet("background-color:#70db70") # green
- else: self.indicator.setStyleSheet("background-color:#ff8c1a") # orange
+ if state == 0.5:
+ self.indicator.setStyleSheet("background-color:#FFFF00") # yellow
+ self.indicator.setToolTip('Value written but not read')
+ elif state:
+ self.indicator.setStyleSheet("background-color:#70db70") # green
+ self.indicator.setToolTip('Value read')
+ else:
+ self.indicator.setStyleSheet("background-color:#ff8c1a") # orange
+ self.indicator.setToolTip('Value not up-to-date')
def read(self):
""" Start a new thread to READ the associated variable """
- self.setValueKnownState(False)
- self.gui.threadManager.start(self, 'read')
+ if not self.isDisabled():
+ self.setValueKnownState(False)
+ self.gui.threadManager.start(self, 'read')
def write(self):
""" Start a new thread to WRITE the associated variable """
- value = self.readGui()
- if value is not None:
- self.gui.threadManager.start(self, 'write', value=value)
+ if not self.isDisabled():
+ value = self.readGui()
+ if value is not None:
+ self.gui.threadManager.start(self, 'write', value=value)
+
+ def valueWrited(self, value: Any):
+ """ Function call when the value displayed in not sure anymore.
+ The value has been modified either in the GUI (but not sent)
+ or by command line.
+ If variable not readable, write the value sent to the GUI """
+ # BUG: I got an error when changing emit_write to set value, need to reproduce it
+ try:
+ self.writeGui(value)
+ self.setValueKnownState(0.5)
+ except Exception as e:
+ self.gui.setStatus(f"SHOULD NOT RAISE ERROR: {e}", 10000, False)
def valueEdited(self):
""" Function call when the value displayed in not sure anymore.
- The value has been modified either in the GUI (but not sent) or by command line """
+ The value has been modified either in the GUI (but not sent)
+ or by command line """
self.setValueKnownState(False)
+ def readButtonCheckEdited(self):
+ state = bool(self.readButtonCheck.isChecked())
+ self.readButton.setEnabled(state)
+
def menu(self, position):
""" This function provides the menu when the user right click on an item """
if not self.isDisabled():
@@ -362,12 +651,12 @@ def menu(self, position):
def saveValue(self):
filename = QtWidgets.QFileDialog.getSaveFileName(
self.gui, f"Save {self.variable.name} value", os.path.join(
- paths.USER_LAST_CUSTOM_FOLDER,f'{self.variable.address()}.txt'),
+ PATHS['last_folder'],f'{self.variable.address()}.txt'),
filter=SUPPORTED_EXTENSION)[0]
path = os.path.dirname(filename)
if path != '':
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
try:
self.gui.setStatus(
f"Saving value of {self.variable.name}...", 5000)
@@ -376,7 +665,7 @@ def saveValue(self):
f"Value of {self.variable.name} successfully read and save at {filename}",
5000)
except Exception as e:
- self.gui.setStatus(f"An error occured: {str(e)}", 10000, False)
+ self.gui.setStatus(f"An error occured: {e}", 10000, False)
# Signals can be emitted only from QObjects
@@ -387,6 +676,6 @@ def emit_read(self, value):
self.read.emit(value)
class WriteSignal(QtCore.QObject):
- writed = QtCore.Signal()
- def emit_write(self):
- self.writed.emit()
+ writed = QtCore.Signal(object)
+ def emit_write(self, value):
+ self.writed.emit(value)
diff --git a/autolab/core/gui/scanning/config.py b/autolab/core/gui/scanning/config.py
index bd2cf47f..3a555742 100644
--- a/autolab/core/gui/scanning/config.py
+++ b/autolab/core/gui/scanning/config.py
@@ -9,17 +9,23 @@
import datetime
import os
import math as m
-from typing import Any, Tuple, List, Dict
+from typing import Any, Tuple, List, Dict, Union
from collections import OrderedDict
import numpy as np
import pandas as pd
from qtpy import QtWidgets, QtCore
-from .. import variables
-from ...utilities import (boolean, str_to_array, array_to_str,
- str_to_dataframe, dataframe_to_str, create_array)
-from ... import paths, devices, config
+from ...config import get_scanner_config
+from ...elements import Variable as Variable_og
+from ...elements import Action
+from ...devices import DEVICES, list_loaded_devices, get_element_by_address
+from ...utilities import (boolean, str_to_array, array_to_str, create_array,
+ str_to_dataframe, dataframe_to_str, str_to_data,
+ str_to_tuple)
+from ...variables import (get_variable, has_eval, is_Variable, eval_variable,
+ remove_from_config, update_from_config, VARIABLES)
+from ...paths import PATHS
from .... import __version__
@@ -71,7 +77,7 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self.gui = gui
# Import Autolab config
- scanner_config = config.get_scanner_config()
+ scanner_config = get_scanner_config()
self.precision = scanner_config['precision']
# Initializing configuration values
@@ -82,6 +88,34 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self._old_variables = [] # To update variable menu
+ def ask_get_element_by_address(self, device_name: str, address: str):
+ """ Wrap of :meth:`get_element_by_address` to ask user if want to
+ instantiate device if not already instantiated.
+ Returns the element at the given address. """
+ if device_name not in DEVICES:
+ msg_box = QtWidgets.QMessageBox(self.gui)
+ msg_box.setWindowTitle(f"Device {device_name}")
+ msg_box.setText(f"Instantiate device {device_name}?")
+ msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok
+ | QtWidgets.QMessageBox.Cancel)
+ msg_box.show()
+ if msg_box.exec_() == QtWidgets.QMessageBox.Cancel:
+ raise ValueError(f'Refused {device_name} instantiation')
+
+ element = get_element_by_address(address)
+
+ return element
+
+ def update_loaded_devices(self, already_loaded_devices: list):
+ """ Refresh GUI with the new loaded devices """
+ for device in (set(list_loaded_devices()) - set(already_loaded_devices)):
+ item_list = self.gui.mainGui.tree.findItems(
+ device, QtCore.Qt.MatchExactly, 0)
+
+ if len(item_list) == 1:
+ item = item_list[0]
+ self.gui.mainGui.itemClicked(item)
+
# NAMES
###########################################################################
@@ -139,8 +173,8 @@ def updateVariableConfig(self, new_variables: List[Tuple[str, Any]] = None):
if new_variables is None:
new_variables = self.getConfigVariables()
remove_variables = list(set(self._old_variables) - set(new_variables))
- variables.remove_from_config(remove_variables)
- variables.update_from_config(new_variables)
+ remove_from_config(remove_variables)
+ update_from_config(new_variables)
self._old_variables = new_variables
def addNewConfig(self):
@@ -194,7 +228,9 @@ def renameRecipe(self, existing_recipe_name: str, new_recipe_name: str):
if new_recipe_name == existing_recipe_name:
return
if existing_recipe_name not in self.recipeNameList():
- raise ValueError(f'should not be possible to select a non existing recipe_name: {existing_recipe_name} not in {self.recipeNameList()}')
+ raise ValueError(
+ 'should not be possible to select a non existing recipe_name: ' \
+ f'{existing_recipe_name} not in {self.recipeNameList()}')
new_recipe_name = self.getUniqueNameRecipe(new_recipe_name)
old_config = self.config
@@ -216,7 +252,7 @@ def renameRecipe(self, existing_recipe_name: str, new_recipe_name: str):
self.gui.selectParameter_comboBox.setCurrentIndex(prev_index_param)
def checkConfig(self):
- """ Checks validity of a config. Used before a scan start. """
+ """ Checks validity of a config. Used before a scan start or after a scan pause. """
assert len(self.recipeNameList()) != 0, 'Need a recipe to start a scan!'
one_recipe_active = False
@@ -238,26 +274,30 @@ def checkConfig(self):
for step in recipe_i['recipe']:
if step['stepType'] == 'recipe':
has_sub_recipe = True
- assert step['element'] in self.config, f"Recipe {step['element']} doesn't exist in {recipe_name}!"
+ assert step['element'] in self.config, (
+ f"Recipe {step['element']} doesn't exist in {recipe_name}!")
other_recipe = self.config[step['element']]
- assert len(other_recipe['recipe']) > 0, f"Recipe {step['element']} is empty!"
+ assert len(other_recipe['recipe']) > 0, (
+ f"Recipe {step['element']} is empty!")
list_recipe_new.append(other_recipe)
list_recipe_new.remove(recipe_i)
assert one_recipe_active, "Need at least one active recipe!"
- # Replace closed devices by reopenned one
- for recipe_name in self.recipeNameList():
- for i, step in enumerate(self.config[recipe_name]['recipe']):
- if (step['element']._parent.name in devices.DEVICES
- and not step['element']._parent in devices.DEVICES.values()):
- module_name = step['element']._parent.name
- module = self.gui.mainGui.tree.findItems(
- module_name, QtCore.Qt.MatchExactly)[0].module
- var = module.get_variable(
- self.config[recipe_name]['recipe'][i]['element'].name)
- self.config[recipe_name]['recipe'][i]['element'] = var
+ already_loaded_devices = list_loaded_devices()
+ try:
+ # Replace closed devices by reopened one
+ for recipe_name in self.recipeNameList():
+ for step in (self.stepList(recipe_name)
+ + self.parameterList(recipe_name)):
+ if step['element']:
+ device_name = step['element'].address().split('.')[0]
+ element = self.ask_get_element_by_address(
+ device_name, step['element'].address())
+ step['element'] = element
+ finally:
+ self.update_loaded_devices(already_loaded_devices)
def lastRecipeName(self) -> str:
""" Returns last recipe name """
@@ -305,7 +345,7 @@ def removeParameter(self, recipe_name: str, param_name: str):
self.addNewConfig()
def setParameter(self, recipe_name: str, param_name: str,
- element: devices.Device, newName: str = None):
+ element: Variable_og, newName: str = None):
""" Sets the element provided as the new parameter of the scan.
Add a parameter is no existing parameter """
if not self.gui.scanManager.isStarted():
@@ -420,7 +460,7 @@ def setValues(self, recipe_name: str, param_name: str, values: List[float]):
if not self.gui.scanManager.isStarted():
param = self.getParameter(recipe_name, param_name)
- if variables.has_eval(values) or np.ndim(values) == 1:
+ if has_eval(values) or np.ndim(values) == 1:
param['values'] = values
self.addNewConfig()
@@ -435,7 +475,8 @@ def addRecipeStep(self, recipe_name: str, stepType: str, element,
recipe_name = self.lastRecipeName()
if not self.gui.scanManager.isStarted():
- assert recipe_name in self.recipeNameList(), f'{recipe_name} not in {self.recipeNameList()}'
+ assert recipe_name in self.recipeNameList(), (
+ f'{recipe_name} not in {self.recipeNameList()}')
if name is None:
name = self.getUniqueName(recipe_name, element.name)
@@ -450,16 +491,19 @@ def addRecipeStep(self, recipe_name: str, stepType: str, element,
assert element != recipe_name, "Can't have a recipe in itself: {element}" # safeguard but should be stopped before arriving here
if stepType == 'set': setValue = True
elif stepType == 'action' and element.type in [
- int, float, str, np.ndarray, pd.DataFrame]: setValue = True
+ int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]:
+ setValue = True
else: setValue = False
if setValue:
if value is None:
if element.type in [int, float]: value = 0
elif element.type in [str]: value = ''
+ elif element.type in [bytes]: value = b''
elif element.type in [pd.DataFrame]: value = pd.DataFrame()
elif element.type in [np.ndarray]: value = np.array([])
elif element.type in [bool]: value = False
+ elif element.type in [tuple]: value = ([], -1)
step['value'] = value
self.stepList(recipe_name).append(step)
@@ -478,18 +522,18 @@ def renameRecipeStep(self, recipe_name: str, name: str, newName: str):
""" Renames a step in the scan recipe """
if not self.gui.scanManager.isStarted():
if newName != name:
- pos = self.getRecipeStepPosition(recipe_name, name)
+ step_info = self.getRecipeStep(recipe_name, name)
newName = self.getUniqueName(recipe_name, newName)
- self.stepList(recipe_name)[pos]['name'] = newName
+ step_info['name'] = newName
self.gui._refreshRecipe(recipe_name)
self.addNewConfig()
def setRecipeStepValue(self, recipe_name: str, name: str, value: Any):
""" Sets the value of a step in the scan recipe """
if not self.gui.scanManager.isStarted():
- pos = self.getRecipeStepPosition(recipe_name, name)
- if value is not self.stepList(recipe_name)[pos]['value']:
- self.stepList(recipe_name)[pos]['value'] = value
+ step_info = self.getRecipeStep(recipe_name, name)
+ if value is not step_info['value']:
+ step_info['value'] = value
self.gui._refreshRecipe(recipe_name)
self.addNewConfig()
@@ -598,7 +642,8 @@ def getParameter(self, recipe_name: str, param_name: str) -> dict:
def getParameterPosition(self, recipe_name: str, param_name: str) -> int:
""" Returns the position of a parameter """
- return [i for i, param in enumerate(self.parameterList(recipe_name)) if param['name'] == param_name][0]
+ return [i for i, param in enumerate(self.parameterList(recipe_name))
+ if param['name'] == param_name][0]
def parameterList(self, recipe_name: str) -> List[dict]:
""" Returns the list of parameters in the recipe """
@@ -606,12 +651,11 @@ def parameterList(self, recipe_name: str) -> List[dict]:
def parameterNameList(self, recipe_name: str) -> List[str]:
""" Returns the list of parameter names in the recipe """
- if recipe_name in self.config:
- return [param['name'] for param in self.parameterList(recipe_name)]
- else:
- return []
+ return [param['name'] for param in self.parameterList(recipe_name)] if (
+ recipe_name in self.config) else []
- def getParameterElement(self, recipe_name: str, param_name: str) -> devices.Device:
+ def getParameterElement(
+ self, recipe_name: str, param_name: str) -> Union[None, Variable_og]:
""" Returns the element of a parameter """
param = self.getParameter(recipe_name, param_name)
return param['element']
@@ -648,7 +692,8 @@ def getValues(self, recipe_name: str, param_name: str) -> List[float]:
# Creates the array of values for the parameter
if logScale:
- paramValues = np.logspace(m.log10(startValue), m.log10(endValue), nbpts, endpoint=True)
+ paramValues = np.logspace(
+ m.log10(startValue), m.log10(endValue), nbpts, endpoint=True)
else:
paramValues = np.linspace(startValue, endValue, nbpts, endpoint=True)
@@ -664,20 +709,26 @@ def stepList(self, recipe_name: str) -> List[dict]:
""" Returns the list of steps in the recipe """
return self.config[recipe_name]['recipe']
- def getRecipeStepElement(self, recipe_name: str, name: str) -> devices.Device:
- """ Returns the element of a recipe step """
+ def getRecipeStep(self, recipe_name: str, name: str) -> dict:
+ """ Returns a dictionnary with recipe step information """
pos = self.getRecipeStepPosition(recipe_name, name)
- return self.stepList(recipe_name)[pos]['element']
+ return self.stepList(recipe_name)[pos]
+
+ def getRecipeStepElement(
+ self, recipe_name: str, name: str) -> Union[Variable_og, Action]:
+ """ Returns the element of a recipe step """
+ step_info = self.getRecipeStep(recipe_name, name)
+ return step_info['element']
def getRecipeStepType(self, recipe_name: str, name: str) -> str:
""" Returns the type a recipe step """
- pos = self.getRecipeStepPosition(recipe_name, name)
- return self.stepList(recipe_name)[pos]['stepType']
+ step_info = self.getRecipeStep(recipe_name, name)
+ return step_info['stepType']
def getRecipeStepValue(self, recipe_name: str, name: str) -> Any:
""" Returns the value of a recipe step """
- pos = self.getRecipeStepPosition(recipe_name, name)
- return self.stepList(recipe_name)[pos]['value']
+ step_info = self.getRecipeStep(recipe_name, name)
+ return step_info['value']
def getRecipeStepPosition(self, recipe_name: str, name: str) -> int:
""" Returns the position of a recipe step in the recipe """
@@ -687,7 +738,7 @@ def getParamDataFrame(self, recipe_name: str, param_name: str) -> pd.DataFrame:
""" Returns a pd.DataFrame with 'id' and 'param_name'
columns containing the parameter array """
paramValues = self.getValues(recipe_name, param_name)
- paramValues = variables.eval_variable(paramValues)
+ paramValues = eval_variable(paramValues)
paramValues = create_array(paramValues)
assert isinstance(paramValues, np.ndarray)
data = pd.DataFrame()
@@ -704,7 +755,7 @@ def getConfigVariables(self) -> List[Tuple[str, Any]]:
for recipe_name in reversed(self.recipeNameList()):
for param_name in self.parameterNameList(recipe_name):
values = self.getValues(recipe_name, param_name)
- value = values if variables.has_eval(values) else float(values[0])
+ value = values if has_eval(values) else float(values[0])
listVariable.append((param_name, value))
for step in self.stepList(recipe_name):
if step['stepType'] == 'measure':
@@ -749,7 +800,7 @@ def create_configPars(self) -> dict:
param_pars['address'] = "None"
if 'values' in param:
- if variables.has_eval(param['values']):
+ if has_eval(param['values']):
param_pars['values'] = param['values']
else:
param_pars['values'] = array_to_str(
@@ -777,11 +828,11 @@ def create_configPars(self) -> dict:
if stepType == 'set' or (stepType == 'action'
and config_step['element'].type in [
- int, float, str,
+ int, float, bool, str, bytes, tuple,
np.ndarray, pd.DataFrame]):
value = config_step['value']
- if variables.has_eval(value):
+ if has_eval(value):
valueStr = value
else:
if config_step['element'].type in [np.ndarray]:
@@ -794,6 +845,10 @@ def create_configPars(self) -> dict:
valueStr = f'{value:.{self.precision}g}'
except:
valueStr = f'{value}'
+ elif config_step['element'].type in [bytes]:
+ valueStr = f'{value.decode()}'
+ else: # for tuple and safety
+ valueStr = f'{value}'
pars_recipe_i['recipe'][f'{i+1}_value'] = valueStr
@@ -801,20 +856,21 @@ def create_configPars(self) -> dict:
# Add variables to config
name_var_config = [var[0] for var in self.getConfigVariables()]
- names_var_user = list(variables.VARIABLES)
+ names_var_user = list(VARIABLES)
names_var_to_save = list(set(names_var_user) - set(name_var_config))
var_to_save = {}
for var_name in names_var_to_save:
- var = variables.get_variable(var_name)
+ var = get_variable(var_name)
if var is not None:
- assert variables.is_Variable(var)
+ assert is_Variable(var)
value_raw = var.raw
- if isinstance(value_raw, np.ndarray): valueStr = array_to_str(
+ if isinstance(value_raw, np.ndarray):
+ valueStr = array_to_str(
value_raw, threshold=1000000, max_line_width=9000000)
- elif isinstance(value_raw, pd.DataFrame): valueStr = dataframe_to_str(
- value_raw, threshold=1000000)
+ elif isinstance(value_raw, pd.DataFrame):
+ valueStr = dataframe_to_str(value_raw, threshold=1000000)
elif isinstance(value_raw, (int, float, str)):
try: valueStr = f'{value_raw:.{self.precision}g}'
except: valueStr = f'{value_raw}'
@@ -836,28 +892,33 @@ def import_configPars(self, filename: str, append: bool = False):
try:
with open(filename, "r") as read_file:
configPars = json.load(read_file)
- except Exception as error:
- self.gui.setStatus(f"Impossible to load configuration file: {error}", 10000, False)
+ except Exception as e:
+ self.gui.setStatus(
+ f"Impossible to load configuration file: {e}",
+ 10000, False)
return None
else:
- print("ConfigParser depreciated, now use json. Will convert this config to json if save it.")
- configPars = {s: dict(legacy_configPars.items(s)) for s in legacy_configPars.sections()}
+ print("ConfigParser depreciated, now use json. " \
+ "Will convert this config to json if save it.")
+ configPars = {s: dict(legacy_configPars.items(s))
+ for s in legacy_configPars.sections()}
path = os.path.dirname(filename)
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
self.load_configPars(configPars, append=append)
if not self._got_error: self.addNewConfig()
else:
- self.gui.setStatus(f"Configuration file {filename} doesn't exists", 5000)
+ self.gui.setStatus(
+ f"Configuration file {filename} doesn't exists", 5000)
def load_configPars(self, configPars: dict, append: bool = False):
""" Creates a config representing a scan form a configPars """
self._got_error = False
self.configHistory.active = False
previous_config = self.config.copy() # used to recover old config if error in loading new one
- already_loaded_devices = devices.list_loaded_devices()
+ already_loaded_devices = list_loaded_devices()
try:
# LEGACY <= 1.2
@@ -899,7 +960,9 @@ def load_configPars(self, configPars: dict, append: bool = False):
# Config
config = OrderedDict()
- recipeNameList = [i for i in list(configPars) if i != 'autolab' and i != 'variables'] # to remove 'autolab' from recipe list
+ # to remove 'autolab' and 'variables' from recipe list
+ recipeNameList = [i for i in list(configPars)
+ if i not in ('autolab', 'variables')]
for recipe_num_name in recipeNameList:
@@ -920,38 +983,45 @@ def load_configPars(self, configPars: dict, append: bool = False):
else:
recipe_i['active'] = True # LEGACY <= 1.2.1
- assert 'parameter' in pars_recipe_i, f'Missing parameter in {recipe_name}'
+ assert 'parameter' in pars_recipe_i, (
+ f'Missing parameter in {recipe_name}')
param_list = recipe_i['parameter'] = []
# LEGACY <= 1.2.1
if len(pars_recipe_i['parameter']) != 0:
- if type(list(pars_recipe_i['parameter'].values())[0]) is not dict:
- pars_recipe_i['parameter'] = {'parameter_1': pars_recipe_i['parameter']}
+ if not isinstance(
+ list(pars_recipe_i['parameter'].values())[0],
+ dict):
+ pars_recipe_i['parameter'] = {
+ 'parameter_1': pars_recipe_i['parameter']}
for param_pars_name in pars_recipe_i['parameter']:
param_pars = pars_recipe_i['parameter'][param_pars_name]
param = {}
- assert 'name' in param_pars, f"Missing name to {param_pars}"
+ assert 'name' in param_pars, (
+ f"Missing name to {param_pars}")
param['name'] = param_pars['name']
- assert 'address' in param_pars, f"Missing address to {param_pars}"
+ assert 'address' in param_pars, (
+ f"Missing address to {param_pars}")
if param_pars['address'] == "None": element = None
else:
- element = devices.get_element_by_address(param_pars['address'])
- assert element is not None, f"Parameter {param_pars['address']} not found."
+ device_name = param_pars['address'].split('.')[0]
+ element = self.ask_get_element_by_address(device_name, param_pars['address'])
param['element'] = element
if 'values' in param_pars:
- if variables.has_eval(param_pars['values']):
+ if has_eval(param_pars['values']):
values = param_pars['values']
else:
values = str_to_array(param_pars['values'])
- if not variables.has_eval(values):
- assert np.ndim(values) == 1, f"Values must be one dimension array in parameter: {param['name']}"
+ if not has_eval(values):
+ assert np.ndim(values) == 1, (
+ f"Values must be one dimension array in parameter: {param['name']}")
param['values'] = values
else:
for key in ['nbpts', 'start_value', 'end_value', 'log']:
@@ -982,30 +1052,36 @@ def load_configPars(self, configPars: dict, append: bool = False):
step['name'] = pars_recipe[f'{i}_name']
name = step['name']
- assert f'{i}_steptype' in pars_recipe, f"Missing stepType in step {i} ({name})."
+ assert f'{i}_steptype' in pars_recipe, (
+ f"Missing stepType in step {i} ({name}).")
step['stepType'] = pars_recipe[f'{i}_steptype']
- assert f'{i}_address' in pars_recipe, f"Missing address in step {i} ({name})."
+ assert f'{i}_address' in pars_recipe, (
+ f"Missing address in step {i} ({name}).")
address = pars_recipe[f'{i}_address']
if step['stepType'] == 'recipe':
- assert step['stepType'] != 'recipe', "Removed the recipe in recipe feature!"
+ assert step['stepType'] != 'recipe', (
+ "Removed the recipe in recipe feature!")
element = address
else:
- element = devices.get_element_by_address(address)
+ device_name = address.split('.')[0]
+ element = self.ask_get_element_by_address(device_name, address)
- assert element is not None, f"Address {address} not found for step {i} ({name})."
step['element'] = element
if (step['stepType'] == 'set') or (
step['stepType'] == 'action' and element.type in [
- int, float, str, np.ndarray, pd.DataFrame]):
- assert f'{i}_value' in pars_recipe, f"Missing value in step {i} ({name})."
+ int, float, bool, str, bytes, tuple,
+ np.ndarray, pd.DataFrame]):
+ assert f'{i}_value' in pars_recipe, (
+ f"Missing value in step {i} ({name}).")
value = pars_recipe[f'{i}_value']
try:
try:
- assert variables.has_eval(value), "Need $eval: to evaluate the given string"
+ assert has_eval(value), (
+ "Need $eval: to evaluate the given string")
except:
# Type conversions
if element.type in [int]:
@@ -1014,14 +1090,19 @@ def load_configPars(self, configPars: dict, append: bool = False):
value = float(value)
elif element.type in [str]:
value = str(value)
+ elif element.type in [bytes]:
+ value = value.encode()
elif element.type in [bool]:
value = boolean(value)
+ elif element.type in [tuple]:
+ value = str_to_tuple(value)
elif element.type in [np.ndarray]:
value = str_to_array(value)
elif element.type in [pd.DataFrame]:
value = str_to_dataframe(value)
else:
- assert variables.has_eval(value), "Need $eval: to evaluate the given string"
+ assert has_eval(value), (
+ "Need $eval: to evaluate the given string")
except:
raise ValueError(f"Error with {i}_value = {value}. Expect either {element.type} or device address. Check address or open device first.")
@@ -1032,7 +1113,6 @@ def load_configPars(self, configPars: dict, append: bool = False):
recipe.append(step)
else:
break
-
if append:
for conf in config.values():
recipe_name = conf['name']
@@ -1050,27 +1130,23 @@ def load_configPars(self, configPars: dict, append: bool = False):
add_vars = []
for var_name, raw_value in var_dict.items():
- raw_value = variables.convert_str_to_data(raw_value)
+ if not has_eval(raw_value):
+ raw_value = str_to_data(raw_value)
add_vars.append((var_name, raw_value))
- variables.update_from_config(add_vars)
+ update_from_config(add_vars)
except Exception as error:
self._got_error = True
- self.gui.setStatus(f"Impossible to load configuration file: {error}", 10000, False)
+ self.gui.setStatus(
+ f"Impossible to load configuration file: {error}", 10000, False)
self.config = previous_config
else:
self.gui._resetRecipe()
self.gui.setStatus("Configuration file loaded successfully", 5000)
-
- for device in (set(devices.list_loaded_devices()) - set(already_loaded_devices)):
- item_list = self.gui.mainGui.tree.findItems(device, QtCore.Qt.MatchExactly, 0)
-
- if len(item_list) == 1:
- item = item_list[0]
- self.gui.mainGui.itemClicked(item)
-
- self.configHistory.active = True
+ finally:
+ self.configHistory.active = True
+ self.update_loaded_devices(already_loaded_devices)
# UNDO REDO ACTIONS
###########################################################################
@@ -1094,6 +1170,7 @@ def changeConfig(self):
self.updateUndoRedoButtons()
self.updateVariableConfig()
+ self.gui.setStatus('')
def updateUndoRedoButtons(self):
""" enables/disables undo/redo button depending on history """
diff --git a/autolab/core/gui/scanning/customWidgets.py b/autolab/core/gui/scanning/customWidgets.py
index a5881320..365c9e07 100644
--- a/autolab/core/gui/scanning/customWidgets.py
+++ b/autolab/core/gui/scanning/customWidgets.py
@@ -5,13 +5,18 @@
@author: Jonathan
"""
-from typing import List
+from typing import List, Union
+import numpy as np
+import pandas as pd
from qtpy import QtCore, QtWidgets, QtGui
from ..icons import icons
-from ...devices import Device
-from ...utilities import clean_string
+from ...utilities import clean_string, array_to_str, dataframe_to_str
+from ...elements import Variable as Variable_og
+from ...elements import Action
+from ...variables import has_eval
+from ...config import get_scanner_config
class MyQTreeWidget(QtWidgets.QTreeWidget):
@@ -23,7 +28,7 @@ def __init__(self, parent: QtWidgets.QFrame,
gui: QtWidgets.QMainWindow, recipe_name: str):
self.recipe_name = recipe_name
- self.scanner = gui
+ self.scanner = gui # gui is scanner
super().__init__(parent)
self.setAcceptDrops(True)
@@ -143,7 +148,9 @@ def dropEvent(self, event):
elif variable.readable:
gui.addStepToScanRecipe(self.recipe_name, 'measure', variable)
elif variable.writable:
- gui.addStepToScanRecipe(self.recipe_name, 'set', variable)
+ value = variable.value if variable.type in [tuple] else None
+ gui.addStepToScanRecipe(
+ self.recipe_name, 'set', variable, value=value)
elif variable._element_type == "action":
gui.addStepToScanRecipe(self.recipe_name, 'action', variable)
@@ -187,13 +194,13 @@ def dragLeaveEvent(self, event):
self.setGraphicsEffect(None)
def menu(self, gui: QtWidgets.QMainWindow,
- variable: Device, position: QtCore.QPoint):
+ variable: Union[Variable_og, Action], position: QtCore.QPoint):
""" Provides the menu when the user right click on an item """
menu = QtWidgets.QMenu()
scanMeasureStepAction = menu.addAction("Measure in scan recipe")
- scanMeasureStepAction.setIcon(QtGui.QIcon(icons['measure']))
+ scanMeasureStepAction.setIcon(icons['measure'])
scanSetStepAction = menu.addAction("Set value in scan recipe")
- scanSetStepAction.setIcon(QtGui.QIcon(icons['write']))
+ scanSetStepAction.setIcon(icons['write'])
scanMeasureStepAction.setEnabled(variable.readable)
scanSetStepAction.setEnabled(variable.writable)
choice = menu.exec_(self.viewport().mapToGlobal(position))
@@ -201,7 +208,142 @@ def menu(self, gui: QtWidgets.QMainWindow,
gui.addStepToScanRecipe(self.recipe_name, 'measure', variable)
elif choice == scanSetStepAction:
- gui.addStepToScanRecipe(self.recipe_name, 'set', variable)
+ value = variable.value if variable.type in [tuple] else None
+ gui.addStepToScanRecipe(
+ self.recipe_name, 'set', variable, value=value)
+
+ def keyPressEvent(self, event):
+ ctrl = QtCore.Qt.ControlModifier
+ shift = QtCore.Qt.ShiftModifier
+ mod = event.modifiers()
+ if event.key() == QtCore.Qt.Key_R and mod == ctrl:
+ self.rename_step(event)
+ elif event.key() == QtCore.Qt.Key_C and mod == ctrl:
+ self.copy_step(event)
+ elif event.key() == QtCore.Qt.Key_V and mod == ctrl:
+ self.paste_step(event)
+ elif event.key() == QtCore.Qt.Key_Z and mod == ctrl:
+ # Note: needed to add setFocus to tree on creation to allow multiple ctrl+z
+ self.scanner.configManager.undoClicked()
+ elif (
+ event.key() == QtCore.Qt.Key_Z and mod == (ctrl | shift)
+ ) or (
+ event.key() == QtCore.Qt.Key_Y and mod == ctrl
+ ):
+ self.scanner.configManager.redoClicked()
+ elif (event.key() == QtCore.Qt.Key_Delete):
+ self.remove_step(event)
+ else:
+ super().keyPressEvent(event)
+
+ def rename_step(self, event):
+ if len(self.selectedItems()) == 0:
+ super().keyPressEvent(event)
+ return None
+ item = self.selectedItems()[0] # assume can select only one item
+ self.scanner.recipeDict[self.recipe_name]['recipeManager'].renameStep(item.text(0))
+
+ def copy_step(self, event):
+ if len(self.selectedItems()) == 0:
+ super().keyPressEvent(event)
+ return None
+ item = self.selectedItems()[0] # assume can select only one item
+ self.scanner.recipeDict[self.recipe_name]['recipeManager'].copyStep(item.text(0))
+
+ def paste_step(self, event):
+ self.scanner.recipeDict[self.recipe_name]['recipeManager'].pasteStep()
+
+ def remove_step(self, event):
+ if len(self.selectedItems()) == 0:
+ super().keyPressEvent(event)
+ return None
+ item = self.selectedItems()[0] # assume can select only one item
+ self.scanner.recipeDict[self.recipe_name]['recipeManager'].removeStep(item.text(0))
+
+
+class MyQTreeWidgetItem(QtWidgets.QTreeWidgetItem):
+
+ def __init__(self, itemParent: QtWidgets.QTreeWidget, step: dict,
+ gui: QtWidgets.QMainWindow):
+
+ self.gui = gui
+
+ # Import Autolab config
+ scanner_config = get_scanner_config()
+ self.precision = scanner_config['precision']
+
+ super().__init__(itemParent)
+
+ self.setFlags(self.flags() ^ QtCore.Qt.ItemIsDropEnabled)
+ self.setToolTip(0, step['element']._help)
+
+ # Column 1 : Step name
+ self.setText(0, step['name'])
+
+ # OPTIMIZE: stepType is a bad name. Possible confusion with element type. stepType should be stepAction or just action
+ # Column 2 : Step type
+ if step['stepType'] == 'measure':
+ self.setText(1, 'Measure')
+ self.setIcon(0, icons['measure'])
+ elif step['stepType'] == 'set':
+ self.setText(1, 'Set')
+ self.setIcon(0, icons['write'])
+ elif step['stepType'] == 'action':
+ self.setText(1, 'Do')
+ self.setIcon(0, icons['action'])
+ elif step['stepType'] == 'recipe':
+ self.setText(1, 'Recipe')
+ self.setIcon(0, icons['recipe'])
+
+ # Column 3 : Element address
+ if step['stepType'] == 'recipe':
+ self.setText(2, step['element'])
+ else:
+ self.setText(2, step['element'].address())
+
+ # Column 4 : Icon of element type
+ etype = step['element'].type
+ if etype is int: self.setIcon(3, icons['int'])
+ elif etype is float: self.setIcon(3, icons['float'])
+ elif etype is bool: self.setIcon(3, icons['bool'])
+ elif etype is str: self.setIcon(3, icons['str'])
+ elif etype is bytes: self.setIcon(3, icons['bytes'])
+ elif etype is tuple: self.setIcon(3, icons['tuple'])
+ elif etype is np.ndarray: self.setIcon(3, icons['ndarray'])
+ elif etype is pd.DataFrame: self.setIcon(3, icons['DataFrame'])
+
+ # Column 5 : Value if stepType is 'set'
+ value = step['value']
+ if value is not None:
+ if has_eval(value):
+ self.setText(4, f'{value}')
+ else:
+ try:
+ if step['element'].type in [bool, str, tuple]:
+ self.setText(4, f'{value}')
+ elif step['element'].type in [bytes]:
+ self.setText(4, f"{value.decode()}")
+ elif step['element'].type in [np.ndarray]:
+ value = array_to_str(
+ value, threshold=1000000, max_line_width=100)
+ self.setText(4, f'{value}')
+ elif step['element'].type in [pd.DataFrame]:
+ value = dataframe_to_str(value, threshold=1000000)
+ self.setText(4, f'{value}')
+ else:
+ self.setText(4, f'{value:.{self.precision}g}')
+ except ValueError:
+ self.setText(4, f'{value}')
+
+ # Column 6 : Unit of element
+ unit = step['element'].unit
+ if unit is not None:
+ self.setText(5, str(unit))
+
+ # set AlignTop to all columns
+ for i in range(self.columnCount()):
+ self.setTextAlignment(i, QtCore.Qt.AlignTop)
+ # OPTIMIZE: icon are not aligned with text: https://www.xingyulei.com/post/qt-button-alignment/index.html
class MyQTabWidget(QtWidgets.QTabWidget):
@@ -233,36 +375,36 @@ def menu(self, position: QtCore.QPoint):
if IS_ACTIVE:
activateRecipeAction = menu.addAction("Disable recipe")
- activateRecipeAction.setIcon(QtGui.QIcon(icons['is-enable']))
+ activateRecipeAction.setIcon(icons['is-enable'])
else:
activateRecipeAction = menu.addAction("Enable recipe")
- activateRecipeAction.setIcon(QtGui.QIcon(icons['is-disable']))
+ activateRecipeAction.setIcon(icons['is-disable'])
menu.addSeparator()
renameRecipeAction = menu.addAction("Rename recipe")
- renameRecipeAction.setIcon(QtGui.QIcon(icons['rename']))
+ renameRecipeAction.setIcon(icons['rename'])
removeRecipeAction = menu.addAction("Remove recipe")
- removeRecipeAction.setIcon(QtGui.QIcon(icons['remove']))
+ removeRecipeAction.setIcon(icons['remove'])
menu.addSeparator()
+ # OBSOLETE
recipeLink = self.gui.configManager.getRecipeLink(self.recipe_name)
-
if len(recipeLink) == 1: # A bit too restrictive but do the work
renameRecipeAction.setEnabled(True)
else:
renameRecipeAction.setEnabled(False)
addParameterAction = menu.addAction("Add parameter")
- addParameterAction.setIcon(QtGui.QIcon(icons['add']))
+ addParameterAction.setIcon(icons['add'])
menu.addSeparator()
moveUpRecipeAction = menu.addAction("Move recipe up")
- moveUpRecipeAction.setIcon(QtGui.QIcon(icons['up']))
+ moveUpRecipeAction.setIcon(icons['up'])
moveDownRecipeAction = menu.addAction("Move recipe down")
- moveDownRecipeAction.setIcon(QtGui.QIcon(icons['down']))
+ moveDownRecipeAction.setIcon(icons['down'])
config = self.gui.configManager.config
keys = list(config)
@@ -302,7 +444,7 @@ def renameRecipe(self):
self.recipe_name, newName)
-class parameterQFrame(QtWidgets.QFrame):
+class ParameterQFrame(QtWidgets.QFrame):
# customMimeType = "autolab/MyQTreeWidget-selectedItems"
def __init__(self, parent: QtWidgets.QMainWindow, recipe_name: str, param_name: str):
diff --git a/autolab/core/gui/scanning/data.py b/autolab/core/gui/scanning/data.py
index d45f55c5..ff9b8217 100644
--- a/autolab/core/gui/scanning/data.py
+++ b/autolab/core/gui/scanning/data.py
@@ -18,9 +18,9 @@
import pandas as pd
from qtpy import QtCore, QtWidgets
-from .. import variables
-from ... import config as autolab_config
-from ... import utilities
+from ...config import get_scanner_config
+from ...utilities import boolean, create_array, data_to_dataframe
+from ...variables import has_eval, eval_safely
class DataManager:
@@ -32,15 +32,15 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self.datasets = []
self.queue = Queue()
- scanner_config = autolab_config.get_scanner_config()
- self.save_temp = utilities.boolean(scanner_config["save_temp"])
+ scanner_config = get_scanner_config()
+ self.save_temp = boolean(scanner_config["save_temp"])
# Timer
self.timer = QtCore.QTimer(self.gui)
self.timer.setInterval(33) #30fps
self.timer.timeout.connect(self.sync)
- def getData(self, nbDataset: int, var_list: list,
+ def getData(self, nbDataset: int, var_list: List[str],
selectedData: int = 0, data_name: str = "Scan",
filter_condition: List[dict] = []) -> List[pd.DataFrame]:
""" Returns the required data """
@@ -49,9 +49,10 @@ def getData(self, nbDataset: int, var_list: list,
for i in range(selectedData, nbDataset+selectedData):
if i < len(self.datasets):
- datasets = self.datasets[-(i+1)]
- if recipe_name not in datasets: continue
- dataset = datasets[recipe_name]
+ scanset = self.datasets[-(i+1)]
+ if recipe_name not in scanset: continue
+ if not scanset.display: continue
+ dataset = scanset[recipe_name]
data = None
if data_name == "Scan":
@@ -108,7 +109,7 @@ def getLastSelectedDataset(self) -> Union[dict, None]:
def newDataset(self, config: dict):
""" Creates and returns a new empty dataset """
maximum = 0
- datasets = {}
+ scanset = ScanSet()
if self.save_temp:
FOLDER_TEMP = os.environ['TEMP'] # This variable can be changed at autolab start-up
@@ -126,20 +127,20 @@ def newDataset(self, config: dict):
dataset = Dataset(sub_folder, recipe_name,
config, save_temp=self.save_temp)
- datasets[recipe_name] = dataset
+ scanset[recipe_name] = dataset
# bellow just to know maximum point
nbpts = 1
for parameter in recipe['parameter']:
if 'values' in parameter:
- if variables.has_eval(parameter['values']):
- values = variables.eval_safely(parameter['values'])
+ if has_eval(parameter['values']):
+ values = eval_safely(parameter['values'])
if isinstance(values, str):
nbpts *= 11 # OPTIMIZE: can't know length in this case without doing eval (should not do eval here because can imagine recipe_2 with param set at end of recipe_1)
self.gui.progressBar.setStyleSheet(
"QProgressBar::chunk {background-color: orange;}")
else:
- values = utilities.create_array(values)
+ values = create_array(values)
nbpts *= len(values)
else: nbpts *= len(parameter['values'])
else: nbpts *= parameter['nbpts']
@@ -174,14 +175,14 @@ def newDataset(self, config: dict):
list_recipe_nbpts_new.remove(recipe_nbpts)
- self.datasets.append(datasets)
+ self.datasets.append(scanset)
self.gui.progressBar.setMaximum(maximum)
def sync(self):
""" This function sync the last dataset with the data available in the queue """
# Empty the queue
count = 0
- datasets = self.getLastDataset()
+ scanset = self.getLastDataset()
lenQueue = self.queue.qsize()
# Add scan data to dataset
@@ -190,7 +191,7 @@ def sync(self):
except: break
recipe_name = list(point.values())[0]
- dataset = datasets[recipe_name]
+ dataset = scanset[recipe_name]
dataset.addPoint(point)
count += 1
@@ -200,11 +201,13 @@ def sync(self):
progress = 0
for dataset_name in self.gui.configManager.getRecipeActive():
- progress += len(datasets[dataset_name])
+ progress += len(scanset[dataset_name])
self.gui.progressBar.setValue(progress)
self.gui.save_pushButton.setEnabled(True)
+ if len(self.datasets) != 1:
+ self.gui.save_all_pushButton.setEnabled(True)
# Update plot
self.gui.figureManager.data_comboBoxClicked()
@@ -214,11 +217,11 @@ def updateDisplayableResults(self):
the results that can be plotted """
data_name = self.gui.dataframe_comboBox.currentText()
recipe_name = self.gui.scan_recipe_comboBox.currentText()
- datasets = self.getLastSelectedDataset()
+ scanset = self.getLastSelectedDataset()
- if datasets is None or recipe_name not in datasets: return None
+ if scanset is None or recipe_name not in scanset: return None
- dataset = datasets[recipe_name]
+ dataset = scanset[recipe_name]
data = None
if data_name == "Scan": data = dataset.data
@@ -237,7 +240,7 @@ def updateDisplayableResults(self):
self.gui.variable_y_comboBox.clear()
return None
- try: data = utilities.formatData(data)
+ try: data = data_to_dataframe(data)
except AssertionError: # if np.ndarray of string for example
self.gui.variable_x_comboBox.clear()
self.gui.variable_x2_comboBox.clear()
@@ -289,8 +292,7 @@ def updateDisplayableResults(self):
class Dataset():
- """ Collection of data from a scan """
-
+ """ Collection of data from a recipe """
def __init__(self, folder_dataset_temp: str, recipe_name: str, config: dict,
save_temp: bool = True):
self._data_temp = []
@@ -334,7 +336,7 @@ def __init__(self, folder_dataset_temp: str, recipe_name: str, config: dict,
)
self.data = pd.DataFrame(columns=self.header)
- def getData(self, var_list: list, data_name: str = "Scan",
+ def getData(self, var_list: List[str], data_name: str = "Scan",
dataID: int = 0, filter_condition: List[dict] = []) -> pd.DataFrame:
""" This function returns a dataframe with two columns : the parameter value,
and the requested result value """
@@ -347,7 +349,7 @@ def getData(self, var_list: list, data_name: str = "Scan",
and not isinstance(data, str)
and (len(data.T.shape) == 1 or (
len(data.T.shape) != 0 and data.T.shape[0] == 2))):
- data = utilities.formatData(data)
+ data = data_to_dataframe(data)
else: # Image
return data
@@ -406,7 +408,13 @@ def save(self, filename: str):
dest_folder = os.path.join(dataset_folder, array_name)
if os.path.exists(tmp_folder):
- shutil.copytree(tmp_folder, dest_folder, dirs_exist_ok=True)
+ try:
+ shutil.copytree(tmp_folder, dest_folder,
+ dirs_exist_ok=True) # python >=3.8 only
+ except:
+ if os.path.exists(dest_folder):
+ shutil.rmtree(dest_folder, ignore_errors=True)
+ shutil.copytree(tmp_folder, dest_folder)
else:
# This is only executed if no temp folder is set
if not os.path.exists(dest_folder): os.mkdir(dest_folder)
@@ -467,6 +475,12 @@ def addPoint(self, dataPoint: OrderedDict):
self.data = pd.DataFrame(self._data_temp, columns=self.header)
if self.save_temp:
+ if not os.path.exists(self.folder_dataset_temp):
+ print(f'Warning: {self.folder_dataset_temp} has been created ' \
+ 'but should have been created earlier. ' \
+ 'Check that you have not lost any data',
+ file=sys.stderr)
+ os.mkdir(self.folder_dataset_temp)
if ID == 1:
self.data.tail(1).to_csv(
os.path.join(self.folder_dataset_temp, 'data.txt'),
@@ -479,3 +493,11 @@ def addPoint(self, dataPoint: OrderedDict):
def __len__(self):
""" Returns the number of data point of this dataset """
return len(self.data)
+
+
+class ScanSet(dict):
+ """ Collection of data from a scan """
+ # TODO: use this in scan plot
+ display = True
+ color = 'default'
+ saved = False
diff --git a/autolab/core/gui/scanning/figure.py b/autolab/core/gui/scanning/figure.py
index 86e9a626..31d2031a 100644
--- a/autolab/core/gui/scanning/figure.py
+++ b/autolab/core/gui/scanning/figure.py
@@ -11,14 +11,16 @@
import numpy as np
import pandas as pd
import pyqtgraph as pg
-from qtpy import QtWidgets, QtGui, QtCore
+import pyqtgraph.exporters # Needed for pg.exporters.ImageExporter
+from qtpy import QtWidgets, QtCore
from .display import DisplayValues
+from ..GUI_instances import openPlotter
from ..GUI_utilities import (get_font_size, setLineEditBackground,
pyqtgraph_fig_ax, pyqtgraph_image)
-from ..slider import Slider
-from ..variables import Variable
+from ..GUI_slider import Slider
from ..icons import icons
+from ...variables import Variable
if hasattr(pd.errors, 'UndefinedVariableError'):
@@ -38,7 +40,7 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self.curves = []
self.filter_condition = []
- self._font_size = get_font_size() + 1
+ self._font_size = get_font_size()
# Configure and initialize the figure in the GUI
self.fig, self.ax = pyqtgraph_fig_ax()
@@ -48,11 +50,11 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self.figMap.hide()
self.gui.variable_x_comboBox.activated.connect(
- self.variableChanged)
+ self.axisChanged)
self.gui.variable_x2_comboBox.activated.connect(
- self.variableChanged)
+ self.axisChanged)
self.gui.variable_y_comboBox.activated.connect(
- self.variableChanged)
+ self.axisChanged)
pgv = pg.__version__.split('.')
if int(pgv[0]) == 0 and int(pgv[1]) < 12:
@@ -77,11 +79,17 @@ def __init__(self, gui: QtWidgets.QMainWindow):
self.gui.nbTraces_lineEdit, 'synced', self._font_size)
# Window to show scan data
+ self.gui.displayScanData_pushButton.setIcon(icons['DataFrame'])
self.gui.displayScanData_pushButton.clicked.connect(
self.displayScanDataButtonClicked)
self.gui.displayScanData_pushButton.hide()
self.displayScan = DisplayValues("Scan", size=(500, 300))
- self.displayScan.setWindowIcon(QtGui.QIcon(icons['DataFrame']))
+ self.displayScan.setWindowIcon(icons['DataFrame'])
+
+ self.gui.sendScanData_pushButton.setIcon(icons['plotter'])
+ self.gui.sendScanData_pushButton.clicked.connect(
+ self.sendScanDataButtonClicked)
+ self.gui.sendScanData_pushButton.hide()
# comboBox with scan id
self.gui.data_comboBox.activated.connect(self.data_comboBoxClicked)
@@ -114,8 +122,9 @@ def refresh_filters(self):
self.filter_condition.clear()
if self.gui.checkBoxFilter.isChecked():
- for i in range(self.gui.layoutFilter.count()-1): # last is buttons
- layout = self.gui.layoutFilter.itemAt(i).layout()
+ # OPTIMIZE: Should never based code on widget/layout position. Can instead create dict or other with all information about filter widgets
+ for i in range(self.gui.layoutFilter.count()):
+ layout = self.gui.layoutFilter.itemAt(i).widget().layout()
if layout.count() == 5:
enable = bool(layout.itemAt(0).widget().isChecked())
@@ -164,58 +173,13 @@ def refresh_filters(self):
self.filter_condition.append(filter_i)
- # Change minimum size
- min_width = 6
- min_height = 6
-
- for i in range(self.gui.layoutFilter.count()):
- layout = self.gui.layoutFilter.itemAt(i).layout()
- min_width_temp = 6
- min_height_temp = 6
-
- for j in range(layout.count()):
- item = layout.itemAt(j)
- widget = item.widget()
-
- if widget is not None:
- min_size = widget.minimumSizeHint()
-
- min_width_temp_2 = min_size.width()
- min_height_temp_2 = min_size.height()
-
- if min_width_temp_2 == 0: min_width_temp_2 = 21
- if min_height_temp_2 == 0: min_height_temp_2 = 21
-
- min_width_temp += min_width_temp_2 + 6
- min_height_temp_2 += min_height_temp_2 + 6
-
- if min_height_temp_2 > min_height_temp:
- min_height_temp = min_height_temp_2
-
- min_height += min_height_temp
-
- if min_width_temp > min_width:
- min_width = min_width_temp
-
- min_width += 12
-
- if min_width > 500: min_width = 500
- if min_height < 85: min_height = 85
- if min_height > 210: min_height = 210
-
- self.gui.frameAxis.setMinimumHeight(min_height)
- self.gui.scrollArea_filter.setMinimumWidth(min_width)
- else:
- self.gui.frameAxis.setMinimumHeight(65)
- self.gui.scrollArea_filter.setMinimumWidth(0)
-
self.reloadData()
def refresh_filter_combobox(self, comboBox):
items = []
- for dataset in self.gui.dataManager.datasets:
- for recipe in dataset.values():
- for key in recipe.data.columns:
+ for scanset in self.gui.dataManager.datasets:
+ for dataset in scanset.values():
+ for key in dataset.data.columns:
if key not in items:
items.append(key)
@@ -226,11 +190,11 @@ def refresh_filter_combobox(self, comboBox):
def addFilterClicked(self, filter_type):
""" Add filter condition """
- conditionLayout = QtWidgets.QHBoxLayout()
+ conditionWidget = QtWidgets.QFrame()
+ conditionWidget.setFrameShape(QtWidgets.QFrame.StyledPanel)
+ conditionLayout = QtWidgets.QHBoxLayout(conditionWidget)
filterCheckBox = QtWidgets.QCheckBox()
- filterCheckBox.setMinimumSize(0, 21)
- filterCheckBox.setMaximumSize(16777215, 21)
filterCheckBox.setToolTip('Toggle filter')
filterCheckBox.setCheckState(QtCore.Qt.Checked)
filterCheckBox.stateChanged.connect(self.refresh_filters)
@@ -238,8 +202,6 @@ def addFilterClicked(self, filter_type):
if filter_type in ('standard', 'slider'):
variableComboBox = QtWidgets.QComboBox()
- variableComboBox.setMinimumSize(0, 21)
- variableComboBox.setMaximumSize(16777215, 21)
self.refresh_filter_combobox(variableComboBox)
variableComboBox.activated.connect(self.refresh_filters)
@@ -248,8 +210,6 @@ def addFilterClicked(self, filter_type):
conditionLayout.addWidget(variableComboBox)
filterComboBox = QtWidgets.QComboBox()
- filterComboBox.setMinimumSize(0, 21)
- filterComboBox.setMaximumSize(16777215, 21)
items = ['==', '!=', '<', '<=', '>=', '>']
filterComboBox.addItems(items)
filterComboBox.activated.connect(self.refresh_filters)
@@ -257,8 +217,6 @@ def addFilterClicked(self, filter_type):
if filter_type == 'standard':
valueWidget = QtWidgets.QLineEdit()
- valueWidget.setMinimumSize(0, 21)
- valueWidget.setMaximumSize(16777215, 21)
valueWidget.setText('1')
valueWidget.returnPressed.connect(self.refresh_filters)
valueWidget.textEdited.connect(lambda: setLineEditBackground(
@@ -268,18 +226,15 @@ def addFilterClicked(self, filter_type):
elif filter_type == 'slider':
var = Variable('temp', 1.)
valueWidget = Slider(var)
- min_size = valueWidget.minimumSizeHint()
- valueWidget.setMinimumSize(min_size)
- valueWidget.setMaximumSize(min_size)
valueWidget.minWidget.setText('1.0')
valueWidget.minWidgetValueChanged()
valueWidget.changed.connect(self.refresh_filters)
+ valueWidget.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
+ QtWidgets.QSizePolicy.Fixed)
conditionLayout.addWidget(valueWidget)
elif filter_type == 'custom':
customConditionWidget = QtWidgets.QLineEdit()
- customConditionWidget.setMinimumSize(0, 21)
- customConditionWidget.setMaximumSize(16777215, 21)
customConditionWidget.setToolTip(
"Filter condition can be 'id == 1' '1 <= amplitude <= 2' 'id in (1, 2)'")
customConditionWidget.setText('id == 1')
@@ -291,22 +246,19 @@ def addFilterClicked(self, filter_type):
conditionLayout.addWidget(customConditionWidget)
removePushButton = QtWidgets.QPushButton()
- removePushButton.setMinimumSize(0, 21)
- removePushButton.setMaximumSize(16777215, 21)
- removePushButton.setIcon(QtGui.QIcon(icons['remove']))
+ removePushButton.setIcon(icons['remove'])
removePushButton.clicked.connect(
- lambda: self.remove_filter(conditionLayout))
+ lambda: self.remove_filter(conditionWidget))
conditionLayout.addWidget(removePushButton)
- self.gui.layoutFilter.insertLayout(
- self.gui.layoutFilter.count()-1, conditionLayout)
+ self.gui.layoutFilter.addWidget(conditionWidget)
self.refresh_filters()
- def remove_filter(self, layout):
+ def remove_filter(self, widget):
""" Remove filter condition """
- for j in reversed(range(layout.count())):
- layout.itemAt(j).widget().setParent(None)
- layout.setParent(None)
+ for j in reversed(range(widget.layout().count())):
+ widget.layout().itemAt(j).widget().setParent(None)
+ widget.setParent(None)
self.refresh_filters()
def checkBoxFilterChanged(self):
@@ -324,10 +276,10 @@ def data_comboBoxClicked(self):
""" This function select a dataset """
if len(self.gui.dataManager.datasets) != 0:
self.gui.data_comboBox.show()
- dataset = self.gui.dataManager.getLastSelectedDataset()
+ scanset = self.gui.dataManager.getLastSelectedDataset()
index = self.gui.scan_recipe_comboBox.currentIndex()
- result_names = list(dataset)
+ result_names = list(scanset)
items = [self.gui.scan_recipe_comboBox.itemText(i) for i in range(
self.gui.scan_recipe_comboBox.count())]
@@ -371,14 +323,14 @@ def updateDataframe_comboBox(self):
# Executed each time the queue is read
index = self.gui.dataframe_comboBox.currentIndex()
recipe_name = self.gui.scan_recipe_comboBox.currentText()
- dataset = self.gui.dataManager.getLastSelectedDataset()
+ scanset = self.gui.dataManager.getLastSelectedDataset()
- if dataset is None or recipe_name not in dataset: return None
+ if scanset is None or recipe_name not in scanset: return None
- sub_dataset = dataset[recipe_name]
+ dataset = scanset[recipe_name]
result_names = ["Scan"] + [
- i for i, val in sub_dataset.data_arrays.items() if not isinstance(
+ i for i, val in dataset.data_arrays.items() if not isinstance(
val[0], (str, tuple))] # Remove this condition if want to plot string or tuple: Tuple[List[str], int]
items = [self.gui.dataframe_comboBox.itemText(i) for i in range(
@@ -402,7 +354,8 @@ def setLabel(self, axe: str, value: str):
""" This function changes the label of the given axis """
axes = {'x':'bottom', 'y':'left'}
if value == '': value = ' '
- self.ax.setLabel(axes[axe], value, **{'color':0.4, 'font-size': '12pt'})
+ self.ax.setLabel(axes[axe], value, **{'color': pg.getConfigOption("foreground"),
+ 'font-size': '12pt'})
# PLOT DATA
###########################################################################
@@ -461,10 +414,9 @@ def reloadData(self):
else:
var_to_display = [variable_x, variable_y]
- can_filter = var_to_display != ['', ''] # Allows to differentiate images from scan or arrays. Works only because on dataframe_comboBoxCurrentChanged, updateDisplayableResults is called
+ can_filter = var_to_display not in (['', ''], ['', '', '']) # Allows to differentiate images from scan or arrays. Works only because on dataframe_comboBoxCurrentChanged, updateDisplayableResults is called
filter_condition = self.filter_condition if (
- self.gui.checkBoxFilter.isChecked() and can_filter) else {}
-
+ self.gui.checkBoxFilter.isChecked() and can_filter) else []
data: List[pd.DataFrame] = self.gui.dataManager.getData(
nbtraces_temp, var_to_display,
selectedData=selectedData, data_name=data_name,
@@ -479,6 +431,7 @@ def reloadData(self):
self.refreshDisplayScanData()
if not self.gui.displayScanData_pushButton.isVisible():
self.gui.displayScanData_pushButton.show()
+ self.gui.sendScanData_pushButton.show()
for temp_data in data:
if temp_data is not None: break
@@ -487,6 +440,9 @@ def reloadData(self):
# If plot scan as image
if data_name == "Scan" and self.displayed_as_image:
+ if not self.gui.frameAxis.isVisible():
+ self.gui.frameAxis.show()
+
if not self.gui.variable_x2_comboBox.isVisible():
self.gui.variable_x2_comboBox.show()
self.gui.label_scan_2D.show()
@@ -628,18 +584,23 @@ def reloadData(self):
color = 'r'
alpha = 1
else:
- color = 'k'
+ color = pg.getConfigOption("foreground")
alpha = (true_nbtraces - (len(data) - 1 - i)) / true_nbtraces
+ # TODO: no information about dataset nor scanset in this method! See what could be done
+ # if scanset.color != 'default':
+ # color = scanset.color
+
# Plot
# careful, now that can filter data, need .values to avoid pyqtgraph bug
+ # pyqtgraph 0.11.1 raise hover error if plot deleted
curve = self.ax.plot(x.values, y.values, symbol='x',
symbolPen=color, symbolSize=10,
pen=color, symbolBrush=color)
curve.setAlpha(alpha, False)
self.curves.append(curve)
- def variableChanged(self, index):
+ def axisChanged(self, index):
""" This function is called when the displayed result has been changed
in the combo box. It proceeds to the change. """
if (self.gui.variable_x_comboBox.currentIndex() != -1
@@ -674,19 +635,29 @@ def nbTracesChanged(self):
###########################################################################
def refreshDisplayScanData(self):
recipe_name = self.gui.scan_recipe_comboBox.currentText()
- datasets = self.gui.dataManager.getLastSelectedDataset()
- if datasets is not None and recipe_name in datasets:
+ scanset = self.gui.dataManager.getLastSelectedDataset()
+ if scanset is not None and recipe_name in scanset:
name = f"Scan{self.gui.data_comboBox.currentIndex()+1}"
if self.gui.scan_recipe_comboBox.count() > 1:
name += f", {recipe_name}"
self.displayScan.setWindowTitle(name)
- self.displayScan.refresh(datasets[recipe_name].data)
+ self.displayScan.refresh(scanset[recipe_name].data)
def displayScanDataButtonClicked(self):
- """ This function opens a window showing the scan data for the displayed scan id """
+ """ Opens a window showing the scan data for the displayed scan id """
self.refreshDisplayScanData()
self.displayScan.show()
+ def sendScanDataButtonClicked(self):
+ """ Sends the displayer scan data to plotter """
+ recipe_name = self.gui.scan_recipe_comboBox.currentText()
+ scanset = self.gui.dataManager.getLastSelectedDataset()
+ if scanset is not None and recipe_name in scanset:
+ data = scanset[recipe_name].data
+ scan_name = self.gui.data_comboBox.currentText()
+ data.name = f"{scan_name}_{recipe_name}"
+ openPlotter(variable=scanset[recipe_name].data, has_parent=True)
+
# SAVE FIGURE
###########################################################################
diff --git a/autolab/core/gui/scanning/interface.ui b/autolab/core/gui/scanning/interface.ui
index e558fbab..22f78151 100644
--- a/autolab/core/gui/scanning/interface.ui
+++ b/autolab/core/gui/scanning/interface.ui
@@ -6,22 +6,24 @@
00
- 1154
+ 1214740
-
-
- 9
-
-
-
-
- 9
-
-
-
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
@@ -32,6 +34,12 @@
0
+
+ 0
+
+
+ 0
+ 0
@@ -63,11 +71,11 @@
00
- 362
- 543
+ 379
+ 589
-
+ 0
@@ -80,9 +88,6 @@
0
-
-
-
@@ -98,97 +103,84 @@
QFrame::Raised
-
+
- 0
+ 3
- 0
+ 3
- 0
+ 3
- 0
+ 3
-
-
-
-
- Add recipe
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 0
- 20
-
-
-
-
-
-
-
- Edit:
-
-
-
-
-
-
- Name of recipe that receive variables from Control Panel
-
-
- QComboBox::AdjustToContents
-
-
-
-
-
-
- Name of parameter that receive variables from Control Panel
-
-
- QComboBox::AdjustToContents
-
-
-
-
+
+
+ Add recipe
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+ 82
+ 20
+
+
+
+
+
+
+
+ Edit:
+
+
+
+
+
+
+ Name of recipe that receive variables from Control Panel
+
+
+ QComboBox::AdjustToContents
+
+
+
+
+
+
+ Name of parameter that receive variables from Control Panel
+
+
+ QComboBox::AdjustToContents
+
+
-
-
- 0
- 100
-
-
-
-
- 16777215
- 100
-
- QFrame::StyledPanel
+
+ 9
+
- Start (or stop) the scan
+ Start the scanStart
@@ -197,14 +189,6 @@
-
- true
-
-
-
- 9
-
- Pause (or resume) the scan
@@ -214,22 +198,12 @@
-
-
- Clear any recorded data
-
-
- Clear data
-
-
-
-
-
+
- Save data of the selected scan
+ Stop the scan
- Save
+ Stop
@@ -280,591 +254,573 @@
ArrowCursor
-
+
+ QFrame::StyledPanel
+
+
+ QFrame::Sunken
+
+
+
+ 0
+ 0
+
+ 0
+
+
+ 0
+ 0
-
+ Qt::Vertical
-
+
+
+
+
+
+
+ 0
+ 0
+
+
- QFrame::StyledPanel
+ QFrame::NoFrame
-
+
+
+ 25
+
+
+ 12
+
-
-
-
-
- Select the scan to display
-
-
-
-
-
-
- QComboBox::AdjustToContents
-
-
-
-
-
-
- QComboBox::AdjustToContents
-
-
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Maximum
+
+
+
+ 0
+ 20
+
+
+
+
+
+
+
+ 9
+
-
-
- Display scan data in tabular format
-
-
- Scan data
+
+
+ 0
-
+
+
+
+
+ 75
+ true
+
+
+
+ X axis
+
+
+ Qt::AlignBottom|Qt::AlignHCenter
+
+
+ 5
+
+
+
+
+
+
+ QComboBox::AdjustToContents
+
+
+
+
-
-
- Qt::Horizontal
+
+
+ 0
-
-
- 0
- 20
-
-
-
+
+
+
+
+ 75
+ true
+
+
+
+ Y axis
+
+
+ Qt::AlignBottom|Qt::AlignHCenter
+
+
+ 5
+
+
+
+
+
+
+ QComboBox::AdjustToContents
+
+
+
+
-
-
- Qt::Vertical
+
+
+ 9
-
-
-
-
-
-
- 0
- 65
-
-
-
- QFrame::StyledPanel
-
-
+
+
+
+ 0
+
-
-
- 0
+
+
+ QFrame::StyledPanel
+
+
+ QAbstractScrollArea::AdjustToContents
-
-
-
+
+ true
+
+
+
+
+ 0
+ 0
+ 324
+ 586
+
+
+
+ 0
-
-
-
-
- 75
- true
-
-
-
- X axis
-
-
- Qt::AlignBottom|Qt::AlignHCenter
-
-
- 5
-
-
-
-
-
-
- QComboBox::AdjustToContents
-
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Maximum
+
+ 0
-
-
- 10
- 20
-
+
+ 0
-
-
-
-
-
+ 0
-
-
-
- 75
- true
-
-
-
- Y axis
-
-
- Qt::AlignBottom|Qt::AlignHCenter
-
-
- 5
-
-
-
-
-
-
- QComboBox::AdjustToContents
+
+
+ 8
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 0
- 20
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 5
- 20
-
-
-
-
-
-
-
-
-
- Qt::Vertical
+
+ 8
-
-
- 20
- 5
-
+
+ 8
-
-
-
-
-
- Filter data
-
-
-
-
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 20
- 8
-
-
-
-
-
-
-
-
- 2
- 2
-
-
-
- QFrame::StyledPanel
-
-
- true
-
-
-
+
+
- 0
+ 6
- 0
+ 6
- 0
+ 6
- 0
+ 6
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
-
-
-
-
-
-
-
- QFrame::NoFrame
-
-
- QFrame::Plain
-
-
-
- 6
-
-
- 6
-
-
- 6
-
-
- 6
-
-
-
-
-
-
-
- 0
- 21
-
-
-
-
- 100
- 21
-
-
-
- Add basic
-
-
-
-
-
-
-
- 0
- 21
-
-
-
-
- 100
- 21
-
-
-
- Add slider
-
-
-
-
-
-
-
- 0
- 21
-
-
-
-
- 100
- 21
-
-
-
- Add custom
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 0
- 20
-
-
-
-
-
-
-
-
-
-
+
+
+ Add basic
+
+
+
+
+
+
+ Add slider
+
+
+
+
+
+
+ Add custom
+
+
-
-
+
+
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 5
- 20
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Maximum
-
-
-
- 15
- 20
-
-
-
-
-
-
-
-
-
- Qt::Vertical
-
-
-
- 20
- 0
-
-
-
-
-
-
-
- Show scan data as 2D colormesh
-
-
- 2D plot
-
-
-
-
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 20
- 8
-
-
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Maximum
-
-
-
- 20
- 20
-
-
-
-
-
-
-
- 0
-
-
-
-
- Nb traces
-
-
- Qt::AlignBottom|Qt::AlignHCenter
-
-
- 5
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 70
- 16777215
-
-
-
- Number of visible traces
-
-
- 1
-
-
- Qt::AlignCenter
-
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 5
- 20
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 0
- 20
-
-
-
-
-
-
-
- 0
-
-
-
-
-
- 75
- true
-
-
-
- Y axis
-
-
- Qt::AlignBottom|Qt::AlignHCenter
-
-
- 5
-
-
-
-
-
-
- QComboBox::AdjustToContents
-
-
-
-
-
-
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 8
+
+
+
+
+
+
+
+ Filter data
+
+
+
+
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 8
+
+
+
-
-
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 8
+
+
+
+
+
+
+
+ Show scan data as 2D colormesh
+
+
+ 2D plot
+
+
+
+
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 8
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+ Nb traces
+
+
+ Qt::AlignBottom|Qt::AlignHCenter
+
+
+ 5
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Number of visible traces
+
+
+ 1
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 75
+ true
+
+
+
+ Y axis
+
+
+ Qt::AlignBottom|Qt::AlignHCenter
+
+
+ 5
+
+
+
+
+
+
+ QComboBox::AdjustToContents
+
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+
+
+ Clear any recorded data
+
+
+ Clear all
+
+
+
+
+
+
+ Save data of all scans
+
+
+ Save all
+
+
+
+
+
+
+ Save data of the selected scan
+
+
+ Save scan1
+
+
+
+
+
+
+
+ 10
+ 0
+
+
+
+ Qt::Vertical
+
+
+
+
+
+
+ Select the scan to display
+
+
+ QComboBox::AdjustToContents
+
+
+
+
+
+
+ QComboBox::AdjustToContents
+
+
+
+
+
+
+ QComboBox::AdjustToContents
+
+
+
+
+
+
+ Display scan data in tabular format
+
+
+ Scan data
+
+
+
+
+
+
+ Send scan recipe dataframe to Plotter
+
+
+ Send to Plotter
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 20
+
+
+
+
+
+
+
@@ -877,18 +833,33 @@
00
- 1154
+ 121421
+ scrollArea
+ addRecipe_pushButton
+ selectRecipe_comboBox
+ selectParameter_comboBoxstart_pushButton
- pause_pushButton
- clear_pushButton
- save_pushButtoncontinuous_checkBox
+ data_comboBox
+ scan_recipe_comboBox
+ dataframe_comboBox
+ displayScanData_pushButton
+ variable_x_comboBox
+ variable_x2_comboBox
+ checkBoxFilter
+ scrollArea_filter
+ addFilterPushButton
+ addSliderFilterPushButton
+ addCustomFilterPushButton
+ variable_x2_checkBox
+ nbTraces_lineEdit
+ variable_y_comboBox
diff --git a/autolab/core/gui/scanning/main.py b/autolab/core/gui/scanning/main.py
index 5795d860..0668872c 100644
--- a/autolab/core/gui/scanning/main.py
+++ b/autolab/core/gui/scanning/main.py
@@ -4,6 +4,7 @@
@author: qchat
"""
+from typing import List
import os
import sys
import shutil
@@ -18,29 +19,30 @@
from .recipe import RecipeManager
from .scan import ScanManager
from .data import DataManager
-from .. import variables
from ..icons import icons
-from ... import paths, utilities
-from ... import config as autolab_config
+from ..GUI_instances import openVariablesMenu, openPlotter
+from ...paths import PATHS
+from ...utilities import boolean, SUPPORTED_EXTENSION
+from ...config import get_scanner_config, load_config, change_autolab_config
class Scanner(QtWidgets.QMainWindow):
- def __init__(self, mainGui: QtWidgets.QMainWindow):
+ def __init__(self, parent: QtWidgets.QMainWindow):
- self.mainGui = mainGui
+ self.mainGui = parent
# Configuration of the window
super().__init__()
ui_path = os.path.join(os.path.dirname(__file__), 'interface.ui')
uic.loadUi(ui_path, self)
self.setWindowTitle("AUTOLAB - Scanner")
- self.setWindowIcon(QtGui.QIcon(icons['scanner']))
+ self.setWindowIcon(icons['scanner'])
self.splitter.setSizes([500, 700]) # Set the width of the two main widgets
self.setAcceptDrops(True)
self.recipeDict = {}
- self.variablesMenu = None
self._append = False # option for import config
+ self._copy_step_info = None # used by tree (in recipe) for copy paste step
# Loading of the different centers
self.figureManager = FigureManager(self)
@@ -52,8 +54,9 @@ def __init__(self, mainGui: QtWidgets.QMainWindow):
configMenu = self.menuBar.addMenu('Configuration')
self.importAction = configMenu.addAction('Import configuration')
- self.importAction.setIcon(QtGui.QIcon(icons['import']))
+ self.importAction.setIcon(icons['import'])
self.importAction.triggered.connect(self.importActionClicked)
+ self.importAction.setStatusTip("Import configuration file")
self.openRecentMenu = configMenu.addMenu('Import recent configuration')
self.populateOpenRecent()
@@ -61,49 +64,82 @@ def __init__(self, mainGui: QtWidgets.QMainWindow):
configMenu.addSeparator()
exportAction = configMenu.addAction('Export current configuration')
- exportAction.setIcon(QtGui.QIcon(icons['export']))
+ exportAction.setIcon(icons['export'])
exportAction.triggered.connect(self.exportActionClicked)
+ exportAction.setStatusTip("Export current configuration file")
# Edition menu
editMenu = self.menuBar.addMenu('Edit')
self.undo = editMenu.addAction('Undo')
- self.undo.setIcon(QtGui.QIcon(icons['undo']))
+ self.undo.setIcon(icons['undo'])
+ self.undo.setShortcut(QtGui.QKeySequence("Ctrl+Z"))
self.undo.triggered.connect(self.configManager.undoClicked)
self.undo.setEnabled(False)
+ self.undo.setStatusTip("Revert recipe changes")
self.redo = editMenu.addAction('Redo')
- self.redo.setIcon(QtGui.QIcon(icons['redo']))
+ self.redo.setIcon(icons['redo'])
+ self.redo.setShortcut(QtGui.QKeySequence("Ctrl+Y"))
self.redo.triggered.connect(self.configManager.redoClicked)
self.redo.setEnabled(False)
+ self.redo.setStatusTip("Reapply recipe changes")
- variablesMenuAction = self.menuBar.addAction('Variable')
- variablesMenuAction.triggered.connect(self.openVariablesMenu)
+ guiMenu = self.menuBar.addMenu('Panels')
+
+ plotAction = guiMenu.addAction('Plotter')
+ plotAction.setIcon(icons['plotter'])
+ plotAction.triggered.connect(lambda: openPlotter(has_parent=True))
+ plotAction.setStatusTip('Open the plotter in another window')
+
+ variablesMenuAction = guiMenu.addAction('Variables')
+ variablesMenuAction.setIcon(icons['variables'])
+ variablesMenuAction.triggered.connect(lambda: openVariablesMenu(True))
+ variablesMenuAction.setStatusTip("Open the variable menu in another window")
self.configManager.addRecipe("recipe") # add one recipe by default
self.configManager.undoClicked() # avoid false history
self.setStatus("")
- self.addRecipe_pushButton.clicked.connect(lambda: self.configManager.addRecipe("recipe"))
+ self.addRecipe_pushButton.clicked.connect(
+ lambda: self.configManager.addRecipe("recipe"))
self.selectRecipe_comboBox.activated.connect(self._updateSelectParameter)
# Save button configuration
self.save_pushButton.clicked.connect(self.saveButtonClicked)
self.save_pushButton.setEnabled(False)
+ self.save_all_pushButton.clicked.connect(self.saveAllButtonClicked)
+ self.save_all_pushButton.setEnabled(False)
# Clear button configuration
self.clear_pushButton.clicked.connect(self.clear)
+ self.clear_pushButton.setEnabled(False)
self.variable_x2_comboBox.hide()
self.label_scan_2D.hide()
+ for splitter in (self.splitter, self.splitterGraph):
+ for i in range(splitter.count()):
+ handle = splitter.handle(i)
+ handle.setStyleSheet("background-color: #DDDDDD;")
+ handle.installEventFilter(self)
+
+ def eventFilter(self, obj, event):
+ if event.type() == QtCore.QEvent.Enter:
+ obj.setStyleSheet("background-color: #AAAAAA;") # Hover color
+ elif event.type() == QtCore.QEvent.Leave:
+ obj.setStyleSheet("background-color: #DDDDDD;") # Normal color
+ return super().eventFilter(obj, event)
+
def populateOpenRecent(self):
""" https://realpython.com/python-menus-toolbars/#populating-python-menus-dynamically """
self.openRecentMenu.clear()
- if os.path.exists(paths.HISTORY_CONFIG):
- with open(paths.HISTORY_CONFIG, 'r') as f: filenames = f.readlines()
+ if os.path.exists(PATHS['history_config']):
+ with open(PATHS['history_config'], 'r') as f:
+ filenames = f.readlines()
for filename in reversed(filenames):
filename = filename.rstrip('\n')
action = QtWidgets.QAction(filename, self)
+ action.setIcon(icons['import'])
action.setEnabled(os.path.exists(filename))
action.triggered.connect(
partial(self.configManager.import_configPars, filename))
@@ -111,26 +147,30 @@ def populateOpenRecent(self):
self.openRecentMenu.addSeparator()
action = QtWidgets.QAction('Clear list', self)
+ action.setIcon(icons['remove'])
action.triggered.connect(self.clearOpenRecent)
self.openRecentMenu.addAction(action)
def addOpenRecent(self, filename: str):
- if not os.path.exists(paths.HISTORY_CONFIG):
- with open(paths.HISTORY_CONFIG, 'w') as f: f.write(filename + '\n')
+ if not os.path.exists(PATHS['history_config']):
+ with open(PATHS['history_config'], 'w') as f:
+ f.write(filename + '\n')
else:
- with open(paths.HISTORY_CONFIG, 'r') as f: lines = f.readlines()
+ with open(PATHS['history_config'], 'r') as f:
+ lines = f.readlines()
lines.append(filename)
lines = [line.rstrip('\n')+'\n' for line in lines]
lines = list(reversed(list(dict.fromkeys(reversed(lines))))) # unique names
lines = lines[-10:]
- with open(paths.HISTORY_CONFIG, 'w') as f: f.writelines(lines)
+ with open(PATHS['history_config'], 'w') as f:
+ f.writelines(lines)
self.populateOpenRecent()
def clearOpenRecent(self):
- if os.path.exists(paths.HISTORY_CONFIG):
- try: os.remove(paths.HISTORY_CONFIG)
+ if os.path.exists(PATHS['history_config']):
+ try: os.remove(PATHS['history_config'])
except: pass
self.populateOpenRecent()
@@ -151,31 +191,32 @@ def clear(self):
self.data_comboBox.clear()
self.data_comboBox.hide()
self.save_pushButton.setEnabled(False)
- self.save_pushButton.setText('Save')
+ self.save_all_pushButton.setEnabled(False)
+ self.clear_pushButton.setEnabled(False)
+ self.save_pushButton.setText('Save scan1')
self.progressBar.setValue(0)
self.progressBar.setStyleSheet("")
self.displayScanData_pushButton.hide()
+ self.sendScanData_pushButton.hide()
self.dataframe_comboBox.clear()
self.dataframe_comboBox.addItems(["Scan"])
self.dataframe_comboBox.hide()
self.scan_recipe_comboBox.setCurrentIndex(0)
self.scan_recipe_comboBox.hide()
+ self.refresh_widget(self.clear_pushButton)
- def openVariablesMenu(self):
- if self.variablesMenu is None:
- self.variablesMenu = variables.VariablesMenu(self)
- self.variablesMenu.show()
- else:
- self.variablesMenu.refresh()
-
- def clearVariablesMenu(self):
- """ This clear the variables menu instance reference when quitted """
- self.variablesMenu = None
+ @staticmethod
+ def refresh_widget(widget: QtWidgets.QWidget):
+ """ Avoid intermediate disabled state when badly refreshed """
+ widget.style().unpolish(widget)
+ widget.style().polish(widget)
+ widget.update()
def _addRecipe(self, recipe_name: str):
""" Adds recipe to managers. Called by configManager """
self._update_recipe_combobox() # recreate all and display first index
- self.selectRecipe_comboBox.setCurrentIndex(self.selectRecipe_comboBox.count()-1) # display last index
+ self.selectRecipe_comboBox.setCurrentIndex(
+ self.selectRecipe_comboBox.count()-1) # display last index
self.recipeDict[recipe_name] = {} # order of creation matter
self.recipeDict[recipe_name]['recipeManager'] = RecipeManager(self, recipe_name)
@@ -226,10 +267,12 @@ def _addParameter(self, recipe_name: str, param_name: str):
self.recipeDict[recipe_name]['parameterManager'][param_name] = new_ParameterManager
layoutAll = self.recipeDict[recipe_name]['recipeManager']._layoutAll
- layoutAll.insertWidget(layoutAll.count()-1, new_ParameterManager.mainFrame)
+ layoutAll.insertWidget(
+ layoutAll.count()-1, new_ParameterManager.mainFrame, stretch=0)
self._updateSelectParameter()
- self.selectParameter_comboBox.setCurrentIndex(self.selectParameter_comboBox.count()-1)
+ self.selectParameter_comboBox.setCurrentIndex(
+ self.selectParameter_comboBox.count()-1)
def _removeParameter(self, recipe_name: str, param_name: str):
""" Removes parameter from managers. Called by configManager """
@@ -247,14 +290,17 @@ def _updateSelectParameter(self):
self.selectParameter_comboBox.clear()
if recipe_name != "":
- self.selectParameter_comboBox.addItems(self.configManager.parameterNameList(recipe_name))
+ self.selectParameter_comboBox.addItems(
+ self.configManager.parameterNameList(recipe_name))
self.selectParameter_comboBox.setCurrentIndex(prev_index)
if self.selectParameter_comboBox.currentText() == "":
- self.selectParameter_comboBox.setCurrentIndex(self.selectParameter_comboBox.count()-1)
+ self.selectParameter_comboBox.setCurrentIndex(
+ self.selectParameter_comboBox.count()-1)
#Shows parameter combobox if multi parameters else hide
- if recipe_name != "" and len(self.configManager.parameterList(recipe_name)) > 1:
+ if (recipe_name != ""
+ and len(self.configManager.parameterList(recipe_name)) > 1):
self.selectParameter_comboBox.show()
self.label_selectRecipeParameter.show()
else:
@@ -275,7 +321,8 @@ def _refreshParameterRange(self, recipe_name: str, param_name: str,
recipeDictParam[newName].changeName(newName)
recipeDictParam[newName].refresh()
else:
- print(f"Error: Can't refresh parameter '{param_name}', not found in recipeDictParam '{recipeDictParam}'")
+ print(f"Error: Can't refresh parameter '{param_name}', not found in recipeDictParam '{recipeDictParam}'",
+ file=sys.stderr)
self._updateSelectParameter()
@@ -306,13 +353,23 @@ def __init__(self, parent: QtWidgets.QMainWindow, append: bool):
self.append = append
+ urls = [
+ QtCore.QUrl.fromLocalFile(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)),
+ QtCore.QUrl.fromLocalFile(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DesktopLocation)),
+ QtCore.QUrl.fromLocalFile(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)),
+ QtCore.QUrl.fromLocalFile(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)),
+ QtCore.QUrl.fromLocalFile(os.environ['TEMP']),
+ ]
+
layout = QtWidgets.QVBoxLayout(self)
+ self.setLayout(layout)
file_dialog = QtWidgets.QFileDialog(self, QtCore.Qt.Widget)
file_dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog)
+ file_dialog.setSidebarUrls(urls)
file_dialog.setWindowFlags(file_dialog.windowFlags() & ~QtCore.Qt.Dialog)
- file_dialog.setDirectory(paths.USER_LAST_CUSTOM_FOLDER)
- file_dialog.setNameFilters(["AUTOLAB configuration file (*.conf)", "All Files (*)"])
+ file_dialog.setDirectory(PATHS['last_folder'])
+ file_dialog.setNameFilters(["AUTOLAB configuration file (*.conf)", "Any Files (*)"])
layout.addWidget(file_dialog)
appendCheck = QtWidgets.QCheckBox('Append', self)
@@ -344,7 +401,9 @@ def closeEvent(self, event):
once_or_append = self._append and len(filenames) != 0
for filename in filenames:
- if filename != '': self.configManager.import_configPars(filename, append=self._append)
+ if filename != '':
+ self.configManager.import_configPars(
+ filename, append=self._append)
else:
once_or_append = False
@@ -355,68 +414,91 @@ def exportActionClicked(self):
and export the current scan configuration in it """
filename = QtWidgets.QFileDialog.getSaveFileName(
self, "Export AUTOLAB configuration file",
- os.path.join(paths.USER_LAST_CUSTOM_FOLDER, 'config.conf'),
+ os.path.join(PATHS['last_folder'], 'config.conf'),
"AUTOLAB configuration file (*.conf);;All Files (*)")[0]
if filename != '':
path = os.path.dirname(filename)
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
try:
self.configManager.export(filename)
- self.setStatus(f"Current configuration successfully saved at {filename}", 5000)
+ self.setStatus(
+ f"Current configuration successfully saved at {filename}",
+ 5000)
except Exception as e:
self.setStatus(f"An error occured: {str(e)}", 10000, False)
else:
self.addOpenRecent(filename)
+ def saveAllButtonClicked(self):
+ self._saveData(self.dataManager.datasets)
+
def saveButtonClicked(self):
+ self._saveData([self.dataManager.getLastSelectedDataset()])
+
+ def _saveData(self, all_data: List[dict]):
""" This function is called when the save button is clicked.
It asks a path and starts the procedure to save the data """
filename = QtWidgets.QFileDialog.getSaveFileName(
self, caption="Save data",
- directory=paths.USER_LAST_CUSTOM_FOLDER,
- filter=utilities.SUPPORTED_EXTENSION)[0]
+ directory=PATHS['last_folder'],
+ filter=SUPPORTED_EXTENSION)[0]
path = os.path.dirname(filename)
+ save_folder, extension = os.path.splitext(filename)
+
if path != '':
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
self.setStatus('Saving data...', 5000)
- datasets = self.dataManager.getLastSelectedDataset()
-
- for dataset_name in datasets:
- dataset = datasets[dataset_name]
-
- if len(datasets) == 1:
- filename_recipe = filename
- else:
- dataset_folder, extension = os.path.splitext(filename)
- filename_recipe = f'{dataset_folder}_{dataset_name}{extension}'
- dataset.save(filename_recipe)
-
- scanner_config = autolab_config.get_scanner_config()
- save_config = utilities.boolean(scanner_config["save_config"])
- if save_config:
- dataset_folder, extension = os.path.splitext(filename)
- new_configname = dataset_folder + ".conf"
- config_name = os.path.join(
- os.path.dirname(dataset.folder_dataset_temp), 'config.conf')
+ for scanset in all_data:
+ i = self.dataManager.datasets.index(scanset)
+ scan_name = f'scan{i+1}'
- if os.path.exists(config_name):
- shutil.copy(config_name, new_configname)
+ if len(all_data) == 1:
+ scan_filename = save_folder
+ new_configname = f'{save_folder}.conf'
else:
- if datasets is not self.dataManager.getLastDataset():
- print("Warning: Can't find config for this dataset, save latest config instead", file=sys.stderr)
- self.configManager.export(new_configname) # BUG: it saves latest config instead of dataset config because no record available of previous config. (I did try to put back self.config to dataset but config changes with new dataset (copy doesn't help and deepcopy not possible)
-
- self.addOpenRecent(new_configname)
-
- if utilities.boolean(scanner_config["save_figure"]):
- self.figureManager.save(filename)
-
- self.setStatus(
- f'Last dataset successfully saved in {filename}', 5000)
+ scan_filename = f'{save_folder}_{scan_name}'
+ new_configname = f'{save_folder}_{scan_name}.conf'
+
+ for recipe_name in scanset:
+ dataset = scanset[recipe_name]
+
+ if len(scanset) == 1:
+ filename_recipe = f'{scan_filename}{extension}'
+ else:
+ filename_recipe = f'{scan_filename}_{recipe_name}{extension}'
+ dataset.save(filename_recipe)
+
+ scanset.saved = True
+ scanner_config = get_scanner_config()
+ save_config = boolean(scanner_config["save_config"])
+
+ if save_config:
+ config_name = os.path.join(
+ os.path.dirname(dataset.folder_dataset_temp), 'config.conf')
+
+ if os.path.exists(config_name):
+ shutil.copy(config_name, new_configname)
+ else:
+ if scanset is not self.dataManager.getLastDataset():
+ print("Warning: Can't find config for this dataset, save latest config instead",
+ file=sys.stderr)
+ self.configManager.export(new_configname) # BUG: it saves latest config instead of dataset config because no record available of previous config. (I did try to put back self.config to dataset but config changes with new dataset (copy doesn't help and deepcopy not possible)
+
+ self.addOpenRecent(new_configname)
+
+ if len(all_data) == 1 and boolean(scanner_config["save_figure"]):
+ self.figureManager.save(filename)
+
+ if len(all_data) == 1:
+ self.setStatus(
+ f'{scan_name} successfully saved in {filename}', 5000)
+ else:
+ self.setStatus(
+ f'All scans successfully saved as {save_folder}_[...].txt', 5000)
def dropEvent(self, event):
""" Imports config file if event has url of a file """
@@ -455,7 +537,29 @@ def closeEvent(self, event):
# Stop datamanager timer
self.dataManager.timer.stop()
- # Delete reference of this window in the control center
+ scanner_config = get_scanner_config()
+ ask_close = boolean(scanner_config["ask_close"])
+ if ask_close and not all([scanset.saved for scanset in self.dataManager.datasets]):
+ msg_box = QtWidgets.QMessageBox(self)
+ msg_box.setWindowTitle("Scanner")
+ msg_box.setText("Some data hasn't been saved, close scanner anyway?")
+ msg_box.setStandardButtons(QtWidgets.QMessageBox.Yes
+ | QtWidgets.QMessageBox.No)
+
+ checkbox = QtWidgets.QCheckBox("Don't ask again")
+ msg_box.setCheckBox(checkbox)
+
+ msg_box.show()
+ res = msg_box.exec_()
+
+ autolab_config = load_config('autolab_config')
+ autolab_config['scanner']['ask_close'] = str(not checkbox.isChecked())
+ change_autolab_config(autolab_config)
+
+ if res == QtWidgets.QMessageBox.No:
+ event.ignore() # Prevent the window from closing
+ return None
+
self.mainGui.clearScanner()
for recipe in self.recipeDict.values():
@@ -464,15 +568,12 @@ def closeEvent(self, event):
self.figureManager.close()
- for children in self.findChildren(QtWidgets.QWidget):
- children.deleteLater()
-
# Remove scan variables from VARIABLES
try: self.configManager.updateVariableConfig([])
except: pass
- if self.variablesMenu is not None:
- self.variablesMenu.close()
+ for children in self.findChildren(QtWidgets.QWidget):
+ children.deleteLater()
super().closeEvent(event)
diff --git a/autolab/core/gui/scanning/parameter.py b/autolab/core/gui/scanning/parameter.py
index 5769b18f..20d19bd7 100644
--- a/autolab/core/gui/scanning/parameter.py
+++ b/autolab/core/gui/scanning/parameter.py
@@ -11,11 +11,12 @@
from qtpy import QtCore, QtWidgets, QtGui
from .display import DisplayValues
-from .customWidgets import parameterQFrame
-from .. import variables
-from ..GUI_utilities import get_font_size, setLineEditBackground
+from .customWidgets import ParameterQFrame
+from ..GUI_utilities import (get_font_size, setLineEditBackground, MyLineEdit,
+ MyQComboBox, qt_object_exists)
from ..icons import icons
from ...utilities import clean_string, str_to_array, array_to_str, create_array
+from ...variables import has_eval, has_variable, eval_safely
class ParameterManager:
@@ -30,162 +31,147 @@ def __init__(self, gui: QtWidgets.QMainWindow,
self.point_or_step = "point"
- self._font_size = get_font_size() + 1
+ self._font_size = get_font_size()
- # Parameter frame
- mainFrame = parameterQFrame(self.gui, self.recipe_name, self.param_name)
- mainFrame.setFrameShape(QtWidgets.QFrame.StyledPanel)
- mainFrame.setMinimumSize(0, 32+60)
- mainFrame.setMaximumSize(16777215, 32+60)
- self.mainFrame = mainFrame
+ self.init_ui()
+ # Do refresh at start
+ self.refresh()
+
+ def init_ui(self):
+ # Parameter frame
+ self.mainFrame = ParameterQFrame(
+ self.gui, self.recipe_name, self.param_name)
+ self.mainFrame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.mainFrame.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.mainFrame.customContextMenuRequested.connect(self.rightClick)
+ self.mainFrame.setSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
- ## 1st row frame: Parameter
- frameParameter = QtWidgets.QFrame(mainFrame)
- frameParameter.setMinimumSize(0, 32)
- frameParameter.setMaximumSize(16777215, 32)
- frameParameter.setToolTip(f"Drag and drop a variable or use the right click option of a variable from the control panel to add a recipe to the tree: {self.recipe_name}")
+ # Parameter layout
+ parameterLayout = QtWidgets.QVBoxLayout(self.mainFrame)
+ parameterLayout.setContentsMargins(0,0,0,0)
+ parameterLayout.setSpacing(0)
+ frameParameter = QtWidgets.QFrame()
+ frameParameter.setToolTip(
+ "Drag and drop a variable or use the right click option of a " \
+ "variable from the control panel to add a recipe to the tree: " \
+ f"{self.recipe_name}")
+
+ frameScanRange = QtWidgets.QFrame()
+
+ parameterLayout.addWidget(frameParameter)
+ parameterLayout.addWidget(frameScanRange)
+
+ ## 1st row frame: Parameter
### Name
- parameterName_lineEdit = QtWidgets.QLineEdit('', frameParameter)
- parameterName_lineEdit.setMinimumSize(0, 20)
- parameterName_lineEdit.setMaximumSize(16777215, 20)
- parameterName_lineEdit.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed))
- parameterName_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- parameterName_lineEdit.setToolTip('Name of the parameter, as it will displayed in the data')
- parameterName_lineEdit.textEdited.connect(lambda: setLineEditBackground(
- parameterName_lineEdit, 'edited', self._font_size))
- parameterName_lineEdit.returnPressed.connect(self.nameChanged)
- parameterName_lineEdit.setEnabled(False)
- self.parameterName_lineEdit = parameterName_lineEdit
+ self.parameterName_lineEdit = QtWidgets.QLineEdit('')
+ self.parameterName_lineEdit.setSizePolicy(
+ QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed))
+ self.parameterName_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
+ self.parameterName_lineEdit.setToolTip(
+ 'Name of the parameter, as it will displayed in the data')
+ self.parameterName_lineEdit.textEdited.connect(
+ lambda: setLineEditBackground(
+ self.parameterName_lineEdit, 'edited', self._font_size))
+ self.parameterName_lineEdit.returnPressed.connect(self.nameChanged)
+ self.parameterName_lineEdit.setEnabled(False)
### Address
- parameterAddressIndicator_label = QtWidgets.QLabel("Address:", frameParameter)
- parameterAddressIndicator_label.setMinimumSize(0, 20)
- parameterAddressIndicator_label.setMaximumSize(16777215, 20)
- parameterAddressIndicator_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
- parameterAddress_label = QtWidgets.QLabel("", frameParameter)
- parameterAddress_label.setMinimumSize(0, 20)
- parameterAddress_label.setMaximumSize(16777215, 20)
- parameterAddress_label.setAlignment(QtCore.Qt.AlignCenter)
- parameterAddress_label.setToolTip('Address of the parameter')
- self.parameterAddress_label = parameterAddress_label
+ parameterAddressIndicator_label = QtWidgets.QLabel("Address:")
+ parameterAddressIndicator_label.setAlignment(
+ QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+ self.parameterAddress_label = QtWidgets.QLabel("")
+ self.parameterAddress_label.setAlignment(QtCore.Qt.AlignCenter)
+ self.parameterAddress_label.setToolTip('Address of the parameter')
### Unit
- unit_label = QtWidgets.QLabel("uA", frameParameter)
- unit_label.setMinimumSize(0, 20)
- unit_label.setMaximumSize(16777215, 20)
- unit_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
- self.unit_label = unit_label
+ self.unit_label = QtWidgets.QLabel("uA")
+ self.unit_label.setAlignment(
+ QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
### displayParameter button
self.displayParameter = DisplayValues("Parameter", size=(250, 400))
- self.displayParameter.setWindowIcon(QtGui.QIcon(icons['ndarray']))
- displayParameter_pushButton = QtWidgets.QPushButton("Parameter", frameParameter)
- displayParameter_pushButton.setMinimumSize(0, 23)
- displayParameter_pushButton.setMaximumSize(16777215, 23)
- self.displayParameter_pushButton = displayParameter_pushButton
- self.displayParameter_pushButton.clicked.connect(self.displayParameterButtonClicked)
+ self.displayParameter.setWindowIcon(icons['ndarray'])
+ self.displayParameter_pushButton = QtWidgets.QPushButton("Parameter")
+ self.displayParameter_pushButton.setIcon(icons['ndarray'])
+ self.displayParameter_pushButton.clicked.connect(
+ self.displayParameterButtonClicked)
## 1sr row layout: Parameter
layoutParameter = QtWidgets.QHBoxLayout(frameParameter)
- layoutParameter.addWidget(parameterName_lineEdit)
- layoutParameter.addWidget(unit_label)
+ layoutParameter.addWidget(self.parameterName_lineEdit)
+ layoutParameter.addWidget(self.unit_label)
layoutParameter.addWidget(parameterAddressIndicator_label)
- layoutParameter.addWidget(parameterAddress_label)
- layoutParameter.addWidget(displayParameter_pushButton)
-
- frameScanRange = QtWidgets.QFrame(mainFrame)
- frameScanRange.setMinimumSize(0, 60)
- frameScanRange.setMaximumSize(16777215, 60)
- self.frameScanRange = frameScanRange
+ layoutParameter.addWidget(self.parameterAddress_label)
+ layoutParameter.addWidget(self.displayParameter_pushButton)
## 2nd row frame: Range
- frameScanRange_linLog = QtWidgets.QFrame(frameScanRange)
- frameScanRange_linLog.setMinimumSize(0, 60)
- frameScanRange_linLog.setMaximumSize(16777215, 60)
- self.frameScanRange_linLog = frameScanRange_linLog
+ self.frameScanRange_linLog = QtWidgets.QFrame()
### first grid widgets: start, stop
- labelStart = QtWidgets.QLabel("Start", frameScanRange_linLog)
- start_lineEdit = QtWidgets.QLineEdit('0', frameScanRange_linLog)
- start_lineEdit.setToolTip('Start value of the scan')
- start_lineEdit.setMinimumSize(0, 20)
- start_lineEdit.setMaximumSize(16777215, 20)
- start_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- self.start_lineEdit = start_lineEdit
-
- labelEnd = QtWidgets.QLabel("End", frameScanRange_linLog)
- end_lineEdit = QtWidgets.QLineEdit('10', frameScanRange_linLog)
- end_lineEdit.setMinimumSize(0, 20)
- end_lineEdit.setMaximumSize(16777215, 20)
- end_lineEdit.setToolTip('End value of the scan')
- end_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- self.end_lineEdit = end_lineEdit
+ labelStart = QtWidgets.QLabel("Start", self.frameScanRange_linLog)
+ self.start_lineEdit = QtWidgets.QLineEdit('0', self.frameScanRange_linLog)
+ self.start_lineEdit.setToolTip('Start value of the scan')
+ self.start_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
+
+ labelEnd = QtWidgets.QLabel("End", self.frameScanRange_linLog)
+ self.end_lineEdit = QtWidgets.QLineEdit('10', self.frameScanRange_linLog)
+ self.end_lineEdit.setToolTip('End value of the scan')
+ self.end_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
### first grid layout: start, stop
- startEndGridLayout = QtWidgets.QGridLayout(frameScanRange_linLog)
+ startEndGridLayout = QtWidgets.QGridLayout(self.frameScanRange_linLog)
startEndGridLayout.addWidget(labelStart, 0, 0)
- startEndGridLayout.addWidget(start_lineEdit, 0, 1)
+ startEndGridLayout.addWidget(self.start_lineEdit, 0, 1)
startEndGridLayout.addWidget(labelEnd, 1, 0)
- startEndGridLayout.addWidget(end_lineEdit, 1, 1)
+ startEndGridLayout.addWidget(self.end_lineEdit, 1, 1)
- startEndGridWidget = QtWidgets.QWidget(frameScanRange_linLog)
+ startEndGridWidget = QtWidgets.QFrame(self.frameScanRange_linLog)
startEndGridWidget.setLayout(startEndGridLayout)
### second grid widgets: mean, width
- labelMean = QtWidgets.QLabel("Mean", frameScanRange_linLog)
- mean_lineEdit = QtWidgets.QLineEdit('5', frameScanRange_linLog)
- mean_lineEdit.setMinimumSize(0, 20)
- mean_lineEdit.setMaximumSize(16777215, 20)
- mean_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- self.mean_lineEdit = mean_lineEdit
-
- labelWidth = QtWidgets.QLabel("Width", frameScanRange_linLog)
- width_lineEdit = QtWidgets.QLineEdit('10', frameScanRange_linLog)
- width_lineEdit.setMinimumSize(0, 20)
- width_lineEdit.setMaximumSize(16777215, 20)
- width_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- self.width_lineEdit = width_lineEdit
+ labelMean = QtWidgets.QLabel("Mean", self.frameScanRange_linLog)
+ self.mean_lineEdit = QtWidgets.QLineEdit('5', self.frameScanRange_linLog)
+ self.mean_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
+
+ labelWidth = QtWidgets.QLabel("Width", self.frameScanRange_linLog)
+ self.width_lineEdit = QtWidgets.QLineEdit('10', self.frameScanRange_linLog)
+ self.width_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
### second grid layout: mean, width
- meanWidthGridLayout = QtWidgets.QGridLayout(frameScanRange_linLog)
- meanWidthGridWidget = QtWidgets.QWidget(frameScanRange_linLog)
- meanWidthGridWidget.setLayout(meanWidthGridLayout)
+ meanWidthGridLayout = QtWidgets.QGridLayout(self.frameScanRange_linLog)
meanWidthGridLayout.addWidget(labelMean, 0, 0)
- meanWidthGridLayout.addWidget(mean_lineEdit, 0, 1)
+ meanWidthGridLayout.addWidget(self.mean_lineEdit, 0, 1)
meanWidthGridLayout.addWidget(labelWidth, 1, 0)
- meanWidthGridLayout.addWidget(width_lineEdit, 1, 1)
+ meanWidthGridLayout.addWidget(self.width_lineEdit, 1, 1)
+
+ meanWidthGridWidget = QtWidgets.QFrame(self.frameScanRange_linLog)
+ meanWidthGridWidget.setLayout(meanWidthGridLayout)
### third grid widgets: npts, step, log
- labelNbpts = QtWidgets.QLabel("Nb points", frameScanRange_linLog)
- nbpts_lineEdit = QtWidgets.QLineEdit('11', frameScanRange_linLog)
- nbpts_lineEdit.setMinimumSize(0, 20)
- nbpts_lineEdit.setMaximumSize(16777215, 20)
- nbpts_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- self.nbpts_lineEdit = nbpts_lineEdit
-
- labelStep = QtWidgets.QLabel("Step", frameScanRange_linLog)
- self.labelStep = labelStep
- step_lineEdit = QtWidgets.QLineEdit('1', frameScanRange_linLog)
- step_lineEdit.setMinimumSize(0, 20)
- step_lineEdit.setMaximumSize(16777215, 20)
- step_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- self.step_lineEdit = step_lineEdit
+ labelNbpts = QtWidgets.QLabel("Nb points", self.frameScanRange_linLog)
+ self.nbpts_lineEdit = QtWidgets.QLineEdit('11', self.frameScanRange_linLog)
+ self.nbpts_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
+
+ self.labelStep = QtWidgets.QLabel("Step", self.frameScanRange_linLog)
+ self.step_lineEdit = QtWidgets.QLineEdit('1', self.frameScanRange_linLog)
+ self.step_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
### third grid layout: npts, step, log
- nptsStepGridLayout = QtWidgets.QGridLayout(frameScanRange_linLog)
- nptsStepGridWidget = QtWidgets.QWidget(frameScanRange_linLog)
- nptsStepGridWidget.setLayout(nptsStepGridLayout)
+ nptsStepGridLayout = QtWidgets.QGridLayout(self.frameScanRange_linLog)
nptsStepGridLayout.addWidget(labelNbpts, 0, 0)
- nptsStepGridLayout.addWidget(nbpts_lineEdit, 0, 1)
- nptsStepGridLayout.addWidget(labelStep, 1, 0)
- nptsStepGridLayout.addWidget(step_lineEdit, 1, 1)
+ nptsStepGridLayout.addWidget(self.nbpts_lineEdit, 0, 1)
+ nptsStepGridLayout.addWidget(self.labelStep, 1, 0)
+ nptsStepGridLayout.addWidget(self.step_lineEdit, 1, 1)
+
+ nptsStepGridWidget = QtWidgets.QFrame(self.frameScanRange_linLog)
+ nptsStepGridWidget.setLayout(nptsStepGridLayout)
## 2nd row layout: Range
- layoutScanRange = QtWidgets.QHBoxLayout(frameScanRange_linLog)
+ layoutScanRange = QtWidgets.QHBoxLayout(self.frameScanRange_linLog)
layoutScanRange.setContentsMargins(0,0,0,0)
layoutScanRange.setSpacing(0)
layoutScanRange.addWidget(startEndGridWidget)
@@ -195,70 +181,62 @@ def __init__(self, gui: QtWidgets.QMainWindow,
layoutScanRange.addWidget(nptsStepGridWidget)
## 2nd row bis frame: Values (hidden at start)
- frameScanRange_values = QtWidgets.QFrame(frameScanRange)
- frameScanRange_values.setMinimumSize(0, 60)
- frameScanRange_values.setMaximumSize(16777215, 60)
- self.frameScanRange_values = frameScanRange_values
+ self.frameScanRange_values = QtWidgets.QFrame()
### first grid widgets: values (hidden at start)
- labelValues = QtWidgets.QLabel("Values", frameScanRange_values)
- values_lineEdit = QtWidgets.QLineEdit('[0,1,2,3]', frameScanRange_values)
- values_lineEdit.setToolTip('Values of the scan')
- values_lineEdit.setMinimumSize(0, 20)
- values_lineEdit.setMaximumSize(16777215, 20)
- values_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- values_lineEdit.setMaxLength(10000000)
- self.values_lineEdit = values_lineEdit
+ labelValues = QtWidgets.QLabel("Values", self.frameScanRange_values)
+ self.values_lineEdit = MyLineEdit('[0,1,2,3]', self.frameScanRange_values)
+ self.values_lineEdit.setToolTip('Values of the scan')
+ self.values_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
+ self.values_lineEdit.setMaxLength(10000000)
# TODO: keep eval in values, show evaluated in evaluated
- labelEvaluatedValues = QtWidgets.QLabel("Evaluated values", frameScanRange_values)
- self.labelEvaluatedValues = labelEvaluatedValues
- evaluatedValues_lineEdit = QtWidgets.QLineEdit('[0,1,2,3]', frameScanRange_values)
- evaluatedValues_lineEdit.setToolTip('Evaluated values of the scan')
- evaluatedValues_lineEdit.setMinimumSize(0, 20)
- evaluatedValues_lineEdit.setMaximumSize(16777215, 20)
- evaluatedValues_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
- evaluatedValues_lineEdit.setMaxLength(10000000)
- evaluatedValues_lineEdit.setReadOnly(True)
- evaluatedValues_lineEdit.setStyleSheet(
- "QLineEdit {border: 1px solid #a4a4a4; background-color: #f4f4f4}")
- self.evaluatedValues_lineEdit = evaluatedValues_lineEdit
+ self.labelEvaluatedValues = QtWidgets.QLabel(
+ "Evaluated values", self.frameScanRange_values)
+ self.evaluatedValues_lineEdit = QtWidgets.QLineEdit(
+ '[0,1,2,3]', self.frameScanRange_values)
+ self.evaluatedValues_lineEdit.setToolTip('Evaluated values of the scan')
+ self.evaluatedValues_lineEdit.setAlignment(QtCore.Qt.AlignCenter)
+ self.evaluatedValues_lineEdit.setMaxLength(10000000)
+ self.evaluatedValues_lineEdit.setReadOnly(True)
+ palette = self.evaluatedValues_lineEdit.palette()
+ palette.setColor(
+ QtGui.QPalette.Base, palette.color(QtGui.QPalette.Base).darker(107))
+ self.evaluatedValues_lineEdit.setPalette(palette)
### first grid layout: values (hidden at start)
- valuesGridLayout = QtWidgets.QGridLayout(frameScanRange_values)
+ valuesGridLayout = QtWidgets.QGridLayout(self.frameScanRange_values)
valuesGridLayout.addWidget(labelValues, 0, 0)
- valuesGridLayout.addWidget(values_lineEdit, 0, 1)
- valuesGridLayout.addWidget(labelEvaluatedValues, 1, 0)
- valuesGridLayout.addWidget(evaluatedValues_lineEdit, 1, 1)
+ valuesGridLayout.addWidget(self.values_lineEdit, 0, 1)
+ valuesGridLayout.addWidget(self.labelEvaluatedValues, 1, 0)
+ valuesGridLayout.addWidget(self.evaluatedValues_lineEdit, 1, 1)
- valuesGridWidget = QtWidgets.QWidget(frameScanRange_values)
+ valuesGridWidget = QtWidgets.QFrame(self.frameScanRange_values)
valuesGridWidget.setLayout(valuesGridLayout)
## 2nd row bis layout: Values (hidden at start)
- layoutScanRange_values = QtWidgets.QHBoxLayout(frameScanRange_values)
+ layoutScanRange_values = QtWidgets.QHBoxLayout(self.frameScanRange_values)
layoutScanRange_values.setContentsMargins(0,0,0,0)
layoutScanRange_values.setSpacing(0)
layoutScanRange_values.addWidget(valuesGridWidget)
## 3rd row frame: choice
- frameScanRange_choice = QtWidgets.QFrame(frameScanRange)
- frameScanRange_choice.setMinimumSize(0, 60)
- frameScanRange_choice.setMaximumSize(16777215, 60)
- self.frameScanRange_choice = frameScanRange_choice
+ self.frameScanRange_choice = QtWidgets.QFrame()
### first grid widgets: choice
- comboBoxChoice = QtWidgets.QComboBox(frameScanRange_choice)
- comboBoxChoice.addItems(['Linear', 'Log', 'Custom'])
- self.comboBoxChoice = comboBoxChoice
+ self.comboBoxChoice = MyQComboBox(self.frameScanRange_choice)
+ self.comboBoxChoice.wheel = False
+ self.comboBoxChoice.key = False
+ self.comboBoxChoice.addItems(['Linear', 'Log', 'Custom'])
### first grid layout: choice
- choiceGridLayout = QtWidgets.QGridLayout(frameScanRange_choice)
- choiceGridLayout.addWidget(comboBoxChoice, 0, 0)
+ choiceGridLayout = QtWidgets.QGridLayout(self.frameScanRange_choice)
+ choiceGridLayout.addWidget(self.comboBoxChoice, 0, 0)
- choiceGridWidget = QtWidgets.QWidget(frameScanRange_choice)
+ choiceGridWidget = QtWidgets.QFrame(self.frameScanRange_choice)
choiceGridWidget.setLayout(choiceGridLayout)
## 3rd row layout: choice
- layoutScanRange_choice = QtWidgets.QHBoxLayout(frameScanRange_choice)
+ layoutScanRange_choice = QtWidgets.QHBoxLayout(self.frameScanRange_choice)
layoutScanRange_choice.setContentsMargins(0,0,0,0)
layoutScanRange_choice.setSpacing(0)
layoutScanRange_choice.addWidget(choiceGridWidget)
@@ -266,16 +244,9 @@ def __init__(self, gui: QtWidgets.QMainWindow,
scanRangeLayout = QtWidgets.QHBoxLayout(frameScanRange)
scanRangeLayout.setContentsMargins(0,0,0,0)
scanRangeLayout.setSpacing(0)
- scanRangeLayout.addWidget(frameScanRange_linLog)
- scanRangeLayout.addWidget(frameScanRange_values) # hidden at start
- scanRangeLayout.addWidget(frameScanRange_choice)
-
- # Parameter layout
- parameterLayout = QtWidgets.QVBoxLayout(mainFrame)
- parameterLayout.setContentsMargins(0,0,0,0)
- parameterLayout.setSpacing(0)
- parameterLayout.addWidget(frameParameter)
- parameterLayout.addWidget(frameScanRange)
+ scanRangeLayout.addWidget(self.frameScanRange_linLog)
+ scanRangeLayout.addWidget(self.frameScanRange_values) # hidden at start
+ scanRangeLayout.addWidget(self.frameScanRange_choice)
# Widget 'return pressed' signal connections
self.comboBoxChoice.activated.connect(self.scanRangeComboBoxChanged)
@@ -303,9 +274,6 @@ def __init__(self, gui: QtWidgets.QMainWindow,
self.values_lineEdit.textEdited.connect(lambda: setLineEditBackground(
self.values_lineEdit,'edited', self._font_size))
- # Do refresh at start
- self.refresh()
-
def _removeWidget(self):
if hasattr(self, 'mainFrame'):
try:
@@ -344,18 +312,18 @@ def refresh(self):
if self.gui.configManager.hasCustomValues(self.recipe_name, self.param_name):
raw_values = self.gui.configManager.getValues(self.recipe_name, self.param_name)
- str_raw_values = raw_values if variables.has_eval(
+ str_raw_values = raw_values if has_eval(
raw_values) else array_to_str(raw_values)
- if not variables.has_eval(raw_values):
+ if not has_eval(raw_values):
str_values = str_raw_values
self.evaluatedValues_lineEdit.hide()
self.labelEvaluatedValues.hide()
else:
- values = variables.eval_safely(raw_values)
+ values = eval_safely(raw_values)
try: values = create_array(values)
except: str_values = values
- else: str_values = variables.array_to_str(values)
+ else: str_values = array_to_str(values)
self.evaluatedValues_lineEdit.show()
self.labelEvaluatedValues.show()
@@ -424,7 +392,7 @@ def refresh(self):
f"Wrong format for parameter '{self.param_name}'", 10000, False)
return None
- str_values = variables.array_to_str(paramValues[self.param_name].values)
+ str_values = array_to_str(paramValues[self.param_name].values)
self.evaluatedValues_lineEdit.setText(f'{str_values}')
self.displayParameter.refresh(paramValues)
@@ -436,7 +404,7 @@ def displayParameterButtonClicked(self):
except Exception as e:
self.gui.setStatus(f"Wrong format for parameter '{self.param_name}': {e}", 10000)
return None
- str_values = variables.array_to_str(paramValues[self.param_name].values)
+ str_values = array_to_str(paramValues[self.param_name].values)
self.evaluatedValues_lineEdit.setText(f'{str_values}')
self.displayParameter.refresh(paramValues)
@@ -610,12 +578,12 @@ def valuesChanged(self):
raw_values = self.values_lineEdit.text()
try:
- if not variables.has_eval(raw_values):
+ if not has_eval(raw_values):
raw_values = str_to_array(raw_values)
assert len(raw_values) != 0, "Cannot have empty array"
values = raw_values
- elif not variables.has_variable(raw_values):
- values = variables.eval_safely(raw_values)
+ elif not has_variable(raw_values):
+ values = eval_safely(raw_values)
if not isinstance(values, str):
values = create_array(values)
assert len(values) != 0, "Cannot have empty array"
@@ -630,10 +598,10 @@ def rightClick(self, position: QtCore.QPoint):
addAction = menu.addAction("Add parameter")
- addAction.setIcon(QtGui.QIcon(icons['add']))
+ addAction.setIcon(icons['add'])
removeAction = menu.addAction(f"Remove {self.param_name}")
- removeAction.setIcon(QtGui.QIcon(icons['remove']))
+ removeAction.setIcon(icons['remove'])
choice = menu.exec_(self.mainFrame.mapToGlobal(position))
@@ -647,14 +615,16 @@ def rightClick(self, position: QtCore.QPoint):
def setProcessingState(self, state: str):
""" Sets the background color of the parameter address during the scan """
+ if not qt_object_exists(self.parameterAddress_label):
+ return None
if state == 'idle':
self.parameterAddress_label.setStyleSheet(
- f"font-size: {self._font_size+1}pt;")
+ f"font-size: {self._font_size}pt;")
else:
if state == 'started': color = '#ff8c1a'
if state == 'finished': color = '#70db70'
self.parameterAddress_label.setStyleSheet(
- f"background-color: {color}; font-size: {self._font_size+1}pt;")
+ f"background-color: {color}; font-size: {self._font_size}pt;")
def close(self):
""" Called by scanner on closing """
diff --git a/autolab/core/gui/scanning/recipe.py b/autolab/core/gui/scanning/recipe.py
index 0df7fad2..7fcf0ca9 100644
--- a/autolab/core/gui/scanning/recipe.py
+++ b/autolab/core/gui/scanning/recipe.py
@@ -9,12 +9,13 @@
import pandas as pd
from qtpy import QtCore, QtWidgets, QtGui
-from .customWidgets import MyQTreeWidget, MyQTabWidget
-from .. import variables
+from .customWidgets import MyQTreeWidget, MyQTabWidget, MyQTreeWidgetItem
from ..icons import icons
-from ... import config
+from ..GUI_utilities import MyInputDialog, qt_object_exists
+from ...config import get_scanner_config
+from ...variables import has_eval
from ...utilities import (clean_string, str_to_array, array_to_str,
- str_to_dataframe, dataframe_to_str)
+ str_to_dataframe, dataframe_to_str, str_to_tuple)
class RecipeManager:
@@ -22,32 +23,30 @@ class RecipeManager:
def __init__(self, gui: QtWidgets.QMainWindow, recipe_name: str):
- self.gui = gui
+ self.gui = gui # gui is scanner
self.recipe_name = recipe_name
# Import Autolab config
- scanner_config = config.get_scanner_config()
+ scanner_config = get_scanner_config()
self.precision = scanner_config['precision']
self.defaultItemBackground = None
# Recipe frame
frameRecipe = QtWidgets.QFrame()
- # frameRecipe.setFrameShape(QtWidgets.QFrame.StyledPanel)
# Tree configuration
self.tree = MyQTreeWidget(frameRecipe, self.gui, self.recipe_name)
- self.tree.setHeaderLabels(['Step name', 'Action', 'Element address', 'Type', 'Value', 'Unit'])
+ self.tree.setHeaderLabels(['Step name', 'Action', 'Element address',
+ 'Type', 'Value', 'Unit'])
header = self.tree.header()
header.setMinimumSectionSize(20)
- # header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)
header.setStretchLastSection(False)
header.resizeSection(0, 95)
header.resizeSection(1, 55)
header.resizeSection(2, 115)
header.resizeSection(3, 35)
header.resizeSection(4, 110)
- # header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
header.resizeSection(5, 32)
header.setMaximumSize(16777215, 16777215)
self.tree.itemDoubleClicked.connect(self.itemDoubleClicked)
@@ -56,7 +55,8 @@ def __init__(self, gui: QtWidgets.QMainWindow, recipe_name: str):
self.tree.setDropIndicatorShown(True)
self.tree.setAlternatingRowColors(True)
self.tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
- self.tree.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
+ self.tree.setSizePolicy(
+ QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
self.tree.customContextMenuRequested.connect(self.rightClick)
self.tree.setMinimumSize(0, 200)
self.tree.setMaximumSize(16777215, 16777215)
@@ -67,12 +67,11 @@ def __init__(self, gui: QtWidgets.QMainWindow, recipe_name: str):
# Qframe and QTab for close+parameter+scanrange+recipe
frameAll = QtWidgets.QFrame()
- # frameAll.setFrameShape(QtWidgets.QFrame.StyledPanel)
layoutAll = QtWidgets.QVBoxLayout(frameAll)
layoutAll.setContentsMargins(0,0,0,0)
layoutAll.setSpacing(0)
- layoutAll.addWidget(frameRecipe)
+ layoutAll.addWidget(frameRecipe, stretch=1)
frameAll2 = MyQTabWidget(frameAll, self.gui, self.recipe_name)
self._frame = frameAll2
@@ -94,7 +93,8 @@ def _activateTree(self, active: bool):
self._frame.setTabEnabled(0, bool(active))
def orderChanged(self, event):
- newOrder = [self.tree.topLevelItem(i).text(0) for i in range(self.tree.topLevelItemCount())]
+ newOrder = [self.tree.topLevelItem(i).text(0)
+ for i in range(self.tree.topLevelItemCount())]
self.gui.configManager.setRecipeStepOrder(self.recipe_name, newOrder)
def refresh(self):
@@ -102,82 +102,11 @@ def refresh(self):
self.tree.clear()
for step in self.gui.configManager.stepList(self.recipe_name):
-
# Loading step informations
- item = QtWidgets.QTreeWidgetItem()
- item.setFlags(item.flags() ^ QtCore.Qt.ItemIsDropEnabled)
- item.setToolTip(0, step['element']._help)
-
- # Column 1 : Step name
- item.setText(0, step['name'])
-
- # OPTIMIZE: stepType is a bad name. Possible confusion with element type. stepType should be stepAction or just action
- # Column 2 : Step type
- if step['stepType'] == 'measure':
- item.setText(1, 'Measure')
- item.setIcon(0, QtGui.QIcon(icons['measure']))
- elif step['stepType'] == 'set':
- item.setText(1, 'Set')
- item.setIcon(0, QtGui.QIcon(icons['write']))
- elif step['stepType'] == 'action':
- item.setText(1, 'Do')
- item.setIcon(0, QtGui.QIcon(icons['action']))
- elif step['stepType'] == 'recipe':
- item.setText(1, 'Recipe')
- item.setIcon(0, QtGui.QIcon(icons['recipe']))
-
- # Column 3 : Element address
- if step['stepType'] == 'recipe':
- item.setText(2, step['element'])
- else:
- item.setText(2, step['element'].address())
-
- # Column 4 : Icon of element type
- etype = step['element'].type
- if etype is int: item.setIcon(3, QtGui.QIcon(icons['int']))
- elif etype is float: item.setIcon(3, QtGui.QIcon(icons['float']))
- elif etype is bool: item.setIcon(3, QtGui.QIcon(icons['bool']))
- elif etype is str: item.setIcon(3, QtGui.QIcon(icons['str']))
- elif etype is bytes: item.setIcon(3, QtGui.QIcon(icons['bytes']))
- elif etype is tuple: item.setIcon(3, QtGui.QIcon(icons['tuple']))
- elif etype is np.ndarray: item.setIcon(3, QtGui.QIcon(icons['ndarray']))
- elif etype is pd.DataFrame: item.setIcon(3, QtGui.QIcon(icons['DataFrame']))
-
- # Column 5 : Value if stepType is 'set'
- value = step['value']
- if value is not None:
- if variables.has_eval(value):
- item.setText(4, f'{value}')
- else:
- try:
- if step['element'].type in [bool, str, tuple]:
- item.setText(4, f'{value}')
- elif step['element'].type in [np.ndarray]:
- value = array_to_str(
- value, threshold=1000000, max_line_width=100)
- item.setText(4, f'{value}')
- elif step['element'].type in [pd.DataFrame]:
- value = dataframe_to_str(value, threshold=1000000)
- item.setText(4, f'{value}')
- else:
- item.setText(4, f'{value:.{self.precision}g}')
- except ValueError:
- item.setText(4, f'{value}')
-
- # Column 6 : Unit of element
- unit = step['element'].unit
- if unit is not None:
- item.setText(5, str(unit))
-
- # set AlignTop to all columns
- for i in range(item.columnCount()):
- item.setTextAlignment(i, QtCore.Qt.AlignTop)
- # OPTIMIZE: icon are not aligned with text: https://www.xingyulei.com/post/qt-button-alignment/index.html
-
- # Add item to the tree
- self.tree.addTopLevelItem(item)
+ item = MyQTreeWidgetItem(self.tree, step, self)
self.defaultItemBackground = item.background(0)
+ self.tree.setFocus() # needed for ctrl+z ctrl+y on recipe
# toggle recipe
active = bool(self.gui.configManager.getActive(self.recipe_name))
self.gui._activateRecipe(self.recipe_name, active)
@@ -196,26 +125,47 @@ def rightClick(self, position: QtCore.QPoint):
menuActions = {}
menu = QtWidgets.QMenu()
- menuActions['rename'] = menu.addAction("Rename")
- menuActions['rename'].setIcon(QtGui.QIcon(icons['rename']))
+ menuActions['copy'] = menu.addAction("Copy")
+ menuActions['copy'].setIcon(icons['copy'])
+ menuActions['copy'].setShortcut(QtGui.QKeySequence("Ctrl+C"))
+ menu.addSeparator()
if stepType == 'set' or (stepType == 'action' and element.type in [
- int, float, str, np.ndarray, pd.DataFrame]):
+ int, float, bool, str, tuple, np.ndarray, pd.DataFrame]):
menuActions['setvalue'] = menu.addAction("Set value")
- menuActions['setvalue'].setIcon(QtGui.QIcon(icons['write']))
+ menuActions['setvalue'].setIcon(icons['write'])
menuActions['remove'] = menu.addAction("Remove")
- menuActions['remove'].setIcon(QtGui.QIcon(icons['remove']))
+ menuActions['remove'].setIcon(icons['remove'])
+ menuActions['remove'].setShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Delete))
+
+ menuActions['rename'] = menu.addAction("Rename")
+ menuActions['rename'].setIcon(icons['rename'])
+ menuActions['rename'].setShortcut(QtGui.QKeySequence("Ctrl+R"))
choice = menu.exec_(self.tree.viewport().mapToGlobal(position))
- if 'rename' in menuActions and choice == menuActions['rename']:
+ if choice == menuActions['copy']:
+ self.copyStep(name)
+ elif choice == menuActions['rename']:
self.renameStep(name)
- elif 'remove' in menuActions and choice == menuActions['remove']:
- self.gui.configManager.delRecipeStep(self.recipe_name, name)
+ elif choice == menuActions['remove']:
+ self.removeStep(name)
elif 'setvalue' in menuActions and choice == menuActions['setvalue']:
self.setStepValue(name)
- # else: # TODO: disabled this feature has it is not good in its current state
+ else:
+ menuActions = {}
+ menu = QtWidgets.QMenu()
+ menuActions['paste'] = menu.addAction("Paste")
+ menuActions['paste'].setIcon(icons['paste'])
+ menuActions['paste'].setShortcut(QtGui.QKeySequence("Ctrl+V"))
+ menuActions['paste'].setEnabled(self.gui._copy_step_info is not None)
+ choice = menu.exec_(self.tree.viewport().mapToGlobal(position))
+
+ if choice == menuActions['paste']:
+ self.pasteStep()
+
+ # TODO: disabled this feature has it is not good in its current state
# config = self.gui.configManager.config
# if len(config) > 1:
@@ -225,7 +175,7 @@ def rightClick(self, position: QtCore.QPoint):
# menu = QtWidgets.QMenu()
# for recipe_name in recipe_name_list:
# menuActions[recipe_name] = menu.addAction(f'Add {recipe_name}')
- # menuActions[recipe_name].setIcon(QtGui.QIcon(icons['recipe']))
+ # menuActions[recipe_name].setIcon(icons['recipe'])
# if len(recipe_name_list) != 0:
# choice = menu.exec_(self.tree.viewport().mapToGlobal(position))
@@ -256,6 +206,20 @@ def rightClick(self, position: QtCore.QPoint):
# break
+ def copyStep(self, name: str):
+ """ Copy step information """
+ self.gui._copy_step_info = self.gui.configManager.getRecipeStep(
+ self.recipe_name, name)
+
+ def pasteStep(self):
+ """ Paste step information """
+ step_info = self.gui._copy_step_info
+ if step_info is not None:
+ self.gui.configManager.addRecipeStep(
+ self.recipe_name, step_info['stepType'],
+ step_info['element'], step_info['name'],
+ value=step_info['value'])
+
def renameStep(self, name: str):
""" Prompts the user for a new step name and apply it to the selected step """
newName, state = QtWidgets.QInputDialog.getText(
@@ -267,28 +231,34 @@ def renameStep(self, name: str):
self.gui.configManager.renameRecipeStep(
self.recipe_name, name, newName)
+ def removeStep(self, name: str):
+ self.gui.configManager.delRecipeStep(self.recipe_name, name)
+
def setStepValue(self, name: str):
""" Prompts the user for a new step value and apply it to the selected step """
element = self.gui.configManager.getRecipeStepElement(
self.recipe_name, name)
value = self.gui.configManager.getRecipeStepValue(
self.recipe_name, name)
-
# Default value displayed in the QInputDialog
- if variables.has_eval(value):
+ if has_eval(value):
defaultValue = f'{value}'
else:
if element.type in [np.ndarray]:
defaultValue = array_to_str(value, threshold=1000000, max_line_width=100)
elif element.type in [pd.DataFrame]:
defaultValue = dataframe_to_str(value, threshold=1000000)
+ if element.type in [bytes] and isinstance(value, bytes):
+ defaultValue = f'{value.decode()}'
else:
try:
defaultValue = f'{value:.{self.precision}g}'
except (ValueError, TypeError):
defaultValue = f'{value}'
- main_dialog = variables.VariablesDialog(self.gui, name, defaultValue)
+ main_dialog = MyInputDialog(self.gui, name)
+ main_dialog.setWindowModality(QtCore.Qt.ApplicationModal) # block GUI interaction
+ main_dialog.setTextValue(defaultValue)
main_dialog.show()
if main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
@@ -296,7 +266,7 @@ def setStepValue(self, name: str):
try:
try:
- assert variables.has_eval(value), "Need $eval: to evaluate the given string"
+ assert has_eval(value), "Need $eval: to evaluate the given string"
except:
# Type conversions
if element.type in [int]:
@@ -305,21 +275,23 @@ def setStepValue(self, name: str):
value = float(value)
elif element.type in [str]:
value = str(value)
+ elif element.type in [bytes]:
+ value = value.encode()
elif element.type in [bool]:
if value == "False": value = False
elif value == "True": value = True
value = int(value)
assert value in [0, 1]
value = bool(value)
- # elif element.type in [tuple]:
- # pass # OPTIMIZE: don't know what todo here, key or tuple? how tuple without reading driver, how key without knowing tuple! -> forbid setting tuple in scan
+ elif element.type in [tuple]:
+ value = str_to_tuple(value)
# OPTIMIZE: bad with large data (truncate), but nobody will use it for large data right?
elif element.type in [np.ndarray]:
value = str_to_array(value)
elif element.type in [pd.DataFrame]:
value = str_to_dataframe(value)
else:
- assert variables.has_eval(value), "Need $eval: to evaluate the given string"
+ assert has_eval(value), "Need $eval: to evaluate the given string"
# Apply modification
self.gui.configManager.setRecipeStepValue(
self.recipe_name, name, value)
@@ -340,11 +312,13 @@ def itemDoubleClicked(self, item: QtWidgets.QTreeWidgetItem, column: int):
self.renameStep(name)
elif column == 4:
if stepType == 'set' or (stepType == 'action' and element.type in [
- int, float, str, np.ndarray, pd.DataFrame]):
+ int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame]):
self.setStepValue(name)
def setStepProcessingState(self, name: str, state: str):
""" Sets the background color of a recipe step during the scan """
+ if not qt_object_exists(self.tree):
+ return None
item = self.tree.findItems(name, QtCore.Qt.MatchExactly, 0)[0]
if state is None:
diff --git a/autolab/core/gui/scanning/scan.py b/autolab/core/gui/scanning/scan.py
index c7a8f944..2f3eaeea 100644
--- a/autolab/core/gui/scanning/scan.py
+++ b/autolab/core/gui/scanning/scan.py
@@ -16,10 +16,11 @@
import numpy as np
from qtpy import QtCore, QtWidgets
-from .. import variables
-from ... import paths
-from ..GUI_utilities import qt_object_exists
-from ...utilities import create_array, SUPPORTED_EXTENSION
+from ..GUI_utilities import qt_object_exists, MyInputDialog, MyFileDialog
+from ..GUI_instances import instances
+from ...paths import PATHS
+from ...variables import eval_variable, set_variable, has_eval
+from ...utilities import create_array
class ScanManager:
@@ -30,6 +31,8 @@ def __init__(self, gui: QtWidgets.QMainWindow):
# Start / Stop button configuration
self.gui.start_pushButton.clicked.connect(self.startButtonClicked)
+ self.gui.stop_pushButton.clicked.connect(self.stopButtonClicked)
+ self.gui.stop_pushButton.setEnabled(False)
# Pause / Resume button configuration
self.gui.pause_pushButton.clicked.connect(self.pauseButtonClicked)
@@ -47,9 +50,16 @@ def __init__(self, gui: QtWidgets.QMainWindow):
#############################################################################
def startButtonClicked(self):
- """ Called when the start/stop button is pressed.
+ """ Called when the start button is pressed.
Do the expected action """
- self.stop() if self.isStarted() else self.start()
+ if not self.isStarted():
+ self.start()
+
+ def stopButtonClicked(self):
+ """ Called when the stop button is pressed.
+ Do the expected action """
+ if self.isStarted():
+ self.stop()
def isStarted(self):
""" Returns True or False whether the scan is currently running or not """
@@ -57,56 +67,88 @@ def isStarted(self):
def start(self):
""" Starts a scan """
-
try:
self.gui.configManager.checkConfig() # raise error if config not valid
config = self.gui.configManager.config
except Exception as e:
self.gui.setStatus(f'ERROR The scan cannot start with the current configuration: {str(e)}', 10000, False)
+ return None
+
+ # Should not be possible
+ if self.thread is not None:
+ self.gui.setStatus('ERROR: A scan thread already exists!', 10000, False)
+ try: self.thread.finished()
+ except: pass
+ self.thread = None
+ return None
+
# Only if current config is valid to start a scan
- else:
- # Prepare a new dataset in the datacenter
- self.gui.dataManager.newDataset(config)
-
- # put dataset id onto the combobox and associate data to it
- dataSet_id = len(self.gui.dataManager.datasets)
- self.gui.data_comboBox.addItem(f'Scan{dataSet_id}')
- self.gui.data_comboBox.setCurrentIndex(int(dataSet_id)-1) # trigger the currentIndexChanged event but don't trigger activated
-
- # Start a new thread
- ## Opening
- self.thread = ScanThread(self.gui.dataManager.queue, config)
- ## Signal connections
- self.thread.errorSignal.connect(self.error)
- self.thread.userSignal.connect(self.handler_user_input)
-
- self.thread.startParameterSignal.connect(lambda recipe_name, param_name: self.setParameterProcessingState(recipe_name, param_name, 'started'))
- self.thread.finishParameterSignal.connect(lambda recipe_name, param_name: self.setParameterProcessingState(recipe_name, param_name, 'finished'))
- self.thread.parameterCompletedSignal.connect(lambda recipe_name, param_name: self.resetParameterProcessingState(recipe_name, param_name))
-
- self.thread.startStepSignal.connect(lambda recipe_name, stepName: self.setStepProcessingState(recipe_name, stepName, 'started'))
- self.thread.finishStepSignal.connect(lambda recipe_name, stepName: self.setStepProcessingState(recipe_name, stepName, 'finished'))
- self.thread.recipeCompletedSignal.connect(lambda recipe_name: self.resetStepsProcessingState(recipe_name))
- self.thread.scanCompletedSignal.connect(self.scanCompleted)
-
- self.thread.finished.connect(self.finished)
-
- # Starting
- self.thread.start()
-
- # Start data center timer
- self.gui.dataManager.timer.start()
-
- # Update gui
- self.gui.start_pushButton.setText('Stop')
- self.gui.pause_pushButton.setEnabled(True)
- self.gui.clear_pushButton.setEnabled(False)
- self.gui.progressBar.setValue(0)
- self.gui.importAction.setEnabled(False)
- self.gui.openRecentMenu.setEnabled(False)
- self.gui.undo.setEnabled(False)
- self.gui.redo.setEnabled(False)
- self.gui.setStatus('Scan started!', 5000)
+
+ # Pause monitors if option selected in monitors
+ for var_id in set([id(step['element'])
+ for recipe in config.values()
+ for step in recipe['recipe']+recipe['parameter']]):
+ if var_id in instances['monitors']:
+ monitor = instances['monitors'][var_id]
+ if (monitor.pause_on_scan
+ and not monitor.monitorManager.isPaused()):
+ monitor.pauseButtonClicked()
+
+ # Prepare a new dataset in the datacenter
+ self.gui.dataManager.newDataset(config)
+
+ # put dataset id onto the combobox and associate data to it
+ dataSet_id = len(self.gui.dataManager.datasets)
+ self.gui.data_comboBox.addItem(f'scan{dataSet_id}')
+ self.gui.data_comboBox.setCurrentIndex(int(dataSet_id)-1) # trigger the currentIndexChanged event but don't trigger activated
+
+ # Start a new thread
+ ## Opening
+ self.thread = ScanThread(self.gui.dataManager.queue, config)
+ ## Signal connections
+ self.thread.errorSignal.connect(self.error)
+ self.thread.userSignal.connect(self.handler_user_input)
+
+ self.thread.startParameterSignal.connect(
+ lambda recipe_name, param_name: self.setParameterProcessingState(
+ recipe_name, param_name, 'started'))
+ self.thread.finishParameterSignal.connect(
+ lambda recipe_name, param_name: self.setParameterProcessingState(
+ recipe_name, param_name, 'finished'))
+ self.thread.parameterCompletedSignal.connect(
+ lambda recipe_name, param_name: self.resetParameterProcessingState(
+ recipe_name, param_name))
+
+ self.thread.startStepSignal.connect(
+ lambda recipe_name, stepName: self.setStepProcessingState(
+ recipe_name, stepName, 'started'))
+ self.thread.finishStepSignal.connect(
+ lambda recipe_name, stepName: self.setStepProcessingState(
+ recipe_name, stepName, 'finished'))
+ self.thread.recipeCompletedSignal.connect(
+ lambda recipe_name: self.resetStepsProcessingState(recipe_name))
+ self.thread.scanCompletedSignal.connect(self.scanCompleted)
+
+ self.thread.finished.connect(self.finished)
+
+ # Starting
+ self.thread.start()
+
+ # Start data center timer
+ self.gui.dataManager.timer.start()
+
+ # Update gui
+ self.gui.start_pushButton.setEnabled(False)
+ self.gui.stop_pushButton.setEnabled(True)
+ self.gui.pause_pushButton.setEnabled(True)
+ self.gui.clear_pushButton.setEnabled(False)
+ self.gui.progressBar.setValue(0)
+ self.gui.importAction.setEnabled(False)
+ self.gui.openRecentMenu.setEnabled(False)
+ self.gui.undo.setEnabled(False)
+ self.gui.redo.setEnabled(False)
+ self.gui.setStatus('Scan started!', 5000)
+ self.gui.refresh_widget(self.gui.start_pushButton)
def handler_user_input(self, stepInfos: dict):
unit = stepInfos['element'].unit
@@ -114,41 +156,9 @@ def handler_user_input(self, stepInfos: dict):
if unit in ("open-file", "save-file"):
- class FileDialog(QtWidgets.QDialog):
-
- def __init__(self, parent: QtWidgets.QMainWindow, name: str,
- mode: QtWidgets.QFileDialog):
-
- super().__init__(parent)
- if mode == QtWidgets.QFileDialog.AcceptOpen:
- self.setWindowTitle(f"Open file - {name}")
- elif mode == QtWidgets.QFileDialog.AcceptSave:
- self.setWindowTitle(f"Save file - {name}")
-
- file_dialog = QtWidgets.QFileDialog(self, QtCore.Qt.Widget)
- file_dialog.setAcceptMode(mode)
- file_dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog)
- file_dialog.setWindowFlags(file_dialog.windowFlags() & ~QtCore.Qt.Dialog)
- file_dialog.setDirectory(paths.USER_LAST_CUSTOM_FOLDER)
- file_dialog.setNameFilters(SUPPORTED_EXTENSION.split(";;"))
-
- layout = QtWidgets.QVBoxLayout(self)
- layout.addWidget(file_dialog)
- layout.addStretch()
- layout.setSpacing(0)
- layout.setContentsMargins(0,0,0,0)
-
- self.exec_ = file_dialog.exec_
- self.selectedFiles = file_dialog.selectedFiles
-
- def closeEvent(self, event):
- for children in self.findChildren(QtWidgets.QWidget):
- children.deleteLater()
-
- super().closeEvent(event)
-
if unit == "open-file":
- self.main_dialog = FileDialog(self.gui, name, QtWidgets.QFileDialog.AcceptOpen)
+ self.main_dialog = MyFileDialog(self.gui, name,
+ QtWidgets.QFileDialog.AcceptOpen)
self.main_dialog.show()
if self.main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
@@ -157,7 +167,8 @@ def closeEvent(self, event):
filename = ''
elif unit == "save-file":
- self.main_dialog = FileDialog(self.gui, name, QtWidgets.QFileDialog.AcceptSave)
+ self.main_dialog = MyFileDialog(self.gui, name,
+ QtWidgets.QFileDialog.AcceptSave)
self.main_dialog.show()
if self.main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
@@ -167,43 +178,13 @@ def closeEvent(self, event):
if filename != '':
path = os.path.dirname(filename)
- paths.USER_LAST_CUSTOM_FOLDER = path
+ PATHS['last_folder'] = path
if qt_object_exists(self.main_dialog): self.main_dialog.deleteLater()
if self.thread is not None: self.thread.user_response = filename
elif unit == 'user-input':
-
- class InputDialog(QtWidgets.QDialog):
-
- def __init__(self, parent: QtWidgets.QMainWindow, name: str):
-
- super().__init__(parent)
- self.setWindowTitle(name)
-
- input_dialog = QtWidgets.QInputDialog(self)
- input_dialog.setLabelText(f"Set {name} value")
- input_dialog.setInputMode(QtWidgets.QInputDialog.TextInput)
- input_dialog.setWindowFlags(input_dialog.windowFlags() & ~QtCore.Qt.Dialog)
-
- lineEdit = input_dialog.findChild(QtWidgets.QLineEdit)
- lineEdit.setMaxLength(10000000)
-
- layout = QtWidgets.QVBoxLayout(self)
- layout.addWidget(input_dialog)
- layout.addStretch()
- layout.setSpacing(0)
- layout.setContentsMargins(0,0,0,0)
-
- self.exec_ = input_dialog.exec_
- self.textValue = input_dialog.textValue
-
- def closeEvent(self, event):
- for children in self.findChildren(QtWidgets.QWidget):
- children.deleteLater()
- super().closeEvent(event)
-
- self.main_dialog = InputDialog(self.gui, name)
+ self.main_dialog = MyInputDialog(self.gui, name)
self.main_dialog.show()
if self.main_dialog.exec_() == QtWidgets.QInputDialog.Accepted:
@@ -212,11 +193,21 @@ def closeEvent(self, event):
response = ''
if qt_object_exists(self.main_dialog): self.main_dialog.deleteLater()
+
+ if has_eval(response):
+ try:
+ response = eval_variable(response)
+ except Exception as e:
+ self.thread.errorSignal.emit(e)
+ self.thread.stopFlag.set()
+
if self.thread is not None: self.thread.user_response = response
else:
if self.thread is not None: self.thread.user_response = f"Unknown unit '{unit}'"
def scanCompleted(self):
+ if not qt_object_exists(self.gui.progressBar):
+ return None
self.gui.progressBar.setStyleSheet("")
if self.thread.stopFlag.is_set():
@@ -227,6 +218,16 @@ def scanCompleted(self):
self.gui.progressBar.setMaximum(1)
self.gui.progressBar.setValue(1)
+ # Start monitors if option selected in monitors
+ for var_id in set([id(step['element'])
+ for recipe in self.thread.config.values()
+ for step in recipe['recipe']+recipe['parameter']]):
+ if var_id in instances['monitors']:
+ monitor = instances['monitors'][var_id]
+ if (monitor.start_on_scan
+ and monitor.monitorManager.isPaused()):
+ monitor.pauseButtonClicked()
+
def setStepProcessingState(self, recipe_name: str, stepName: str, state: str):
self.gui.recipeDict[recipe_name]['recipeManager'].setStepProcessingState(stepName, state)
@@ -245,7 +246,8 @@ def stop(self):
self.thread.stopFlag.set()
self.thread.user_response = 'Close' # needed to stop scan
self.resume()
- if self.main_dialog is not None and qt_object_exists(self.main_dialog): self.main_dialog.deleteLater()
+ if self.main_dialog and qt_object_exists(self.main_dialog):
+ self.main_dialog.deleteLater()
self.thread.wait()
# SIGNALS
@@ -255,19 +257,23 @@ def finished(self):
""" This function is called when the scan thread is finished.
It restores the GUI in a ready mode, and start a new scan if in
continuous mode """
- self.gui.start_pushButton.setText('Start')
+ if not qt_object_exists(self.gui.stop_pushButton):
+ return None
+ self.gui.stop_pushButton.setEnabled(False)
self.gui.pause_pushButton.setEnabled(False)
self.gui.clear_pushButton.setEnabled(True)
- self.gui.displayScanData_pushButton.setEnabled(True)
self.gui.importAction.setEnabled(True)
self.gui.openRecentMenu.setEnabled(True)
self.gui.configManager.updateUndoRedoButtons()
self.gui.dataManager.timer.stop()
self.gui.dataManager.sync() # once again to be sure we grabbed every data
self.thread = None
+ self.gui.refresh_widget(self.gui.stop_pushButton)
if self.isContinuousModeEnabled():
self.start()
+ else:
+ self.gui.start_pushButton.setEnabled(True)
def error(self, error: Exception):
""" Called if an error occured during the scan.
@@ -304,10 +310,17 @@ def pause(self):
""" Pauses the scan """
self.thread.pauseFlag.set()
self.gui.dataManager.timer.stop()
+ self.gui.dataManager.sync() # once again to be sure we grabbed every data
self.gui.pause_pushButton.setText('Resume')
def resume(self):
""" Resumes the scan """
+ try:
+ # One possible error is device closed while scan was paused
+ self.gui.configManager.checkConfig() # raise error if config not valid
+ except Exception as e:
+ self.gui.setStatus(f'WARNING The scan cannot resume: {str(e)}', 10000, False)
+ return None
self.thread.pauseFlag.clear()
self.gui.dataManager.timer.start()
self.gui.pause_pushButton.setText('Pause')
@@ -360,7 +373,7 @@ def execRecipe(self, recipe_name: str,
if 'values' in parameter:
paramValues = parameter['values']
try:
- paramValues = variables.eval_variable(paramValues)
+ paramValues = eval_variable(paramValues)
paramValues = create_array(paramValues)
except Exception as e:
self.errorSignal.emit(e)
@@ -376,7 +389,7 @@ def execRecipe(self, recipe_name: str,
else:
paramValues = np.linspace(startValue, endValue, nbpts, endpoint=True)
- variables.set_variable(param_name, paramValues[0])
+ set_variable(param_name, paramValues[0])
paramValues_list.append(paramValues)
ID = 0
@@ -394,7 +407,7 @@ def execRecipe(self, recipe_name: str,
try:
self._source_of_error = None
ID += 1
- variables.set_variable('ID', ID)
+ set_variable('ID', ID)
for parameter, paramValue in zip(
self.config[recipe_name]['parameter'], paramValueList):
@@ -402,7 +415,7 @@ def execRecipe(self, recipe_name: str,
element = parameter['element']
param_name = parameter['name']
- variables.set_variable(param_name, element.type(
+ set_variable(param_name, element.type(
paramValue) if element is not None else paramValue)
# Set the parameter value
@@ -430,12 +443,12 @@ def execRecipe(self, recipe_name: str,
try:
from pyvisa import VisaIOError
except:
- e = f"In recipe '{recipe_name}' for element '{name}'{address}: {e}"
+ e = f"In recipe '{recipe_name}' for step '{name}': {e}"
else:
if str(e) == str(VisaIOError(-1073807339)):
e = f"Timeout reached for device {address}. Acquisition time may be too long. If so, you can increase timeout delay in the driver to avoid this error."
else:
- e = f"In recipe '{recipe_name}' for element '{name}'{address}: {e}"
+ e = f"In recipe '{recipe_name}' for step '{name}': {e}"
self.errorSignal.emit(e)
self.stopFlag.set()
@@ -482,15 +495,16 @@ def processElement(self, recipe_name: str, stepInfos: dict,
if stepType == 'measure':
result = element()
- variables.set_variable(stepInfos['name'], result)
+ set_variable(stepInfos['name'], result)
elif stepType == 'set':
- value = variables.eval_variable(stepInfos['value'])
+ value = eval_variable(stepInfos['value'])
+ if element.type in [bytes] and isinstance(value, str): value = value.encode()
if element.type in [np.ndarray]: value = create_array(value)
element(value)
elif stepType == 'action':
if stepInfos['value'] is not None:
# Open dialog for open file, save file or input text
- if stepInfos['value'] == '':
+ if isinstance(stepInfos['value'], str) and stepInfos['value'] == '':
self.userSignal.emit(stepInfos)
while (not self.stopFlag.is_set()
and self.user_response is None):
@@ -499,7 +513,9 @@ def processElement(self, recipe_name: str, stepInfos: dict,
element(self.user_response)
self.user_response = None
else:
- value = variables.eval_variable(stepInfos['value'])
+ value = eval_variable(stepInfos['value'])
+ if element.type in [bytes] and isinstance(value, str): value = value.encode()
+ if element.type in [np.ndarray]: value = create_array(value)
element(value)
else:
element()
diff --git a/autolab/core/gui/theme/__init__.py b/autolab/core/gui/theme/__init__.py
new file mode 100644
index 00000000..3b1df56d
--- /dev/null
+++ b/autolab/core/gui/theme/__init__.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Thu Sep 5 18:45:37 2024
+
+@author: jonathan
+"""
+
+from typing import Dict
+
+
+theme = {
+ 'dark': {
+ 'text_color': "#f0f0f0",
+ 'text_disabled_color': "#a0a0a0",
+ 'default': "#3c3c3c",
+ 'primary_color': "#3c3c3c",
+ 'secondary_color': "#2c2c2c",
+ 'tertiary_color': "#2e2e2e",
+ 'hover_color': "#555555",
+ 'pressed_color': "#666666",
+ 'selected_color': "#005777",
+ 'border_color': "#5c5c5c",
+ 'main_hover': "#005777",
+ }
+ }
+
+
+def get_theme(theme_name: str) -> Dict[str, str]:
+ return theme.get(theme_name)
+
+
+def create_stylesheet(theme: Dict[str, str]) -> str:
+ stylesheet = f"""
+ QWidget {{
+ background-color: {theme['default']};
+ color: {theme['text_color']};
+ }}
+ QFrame {{
+ background-color: {theme['primary_color']};
+ border-radius: 8px;
+ }}
+ QFrame[frameShape='1'], QFrame[frameShape='2'], QFrame[frameShape='3'], QFrame[frameShape='6'] {{
+ border: 1px solid {theme['border_color']};
+ }}
+ #line_V {{
+ border-left: 1px solid {theme['border_color']};
+ }}
+ #line_H {{
+ border-top: 1px solid {theme['border_color']};
+ }}
+ QPushButton {{
+ background-color: {theme['secondary_color']};
+ border: 1px solid {theme['border_color']};
+ border-radius: 6px;
+ padding: 4px;
+ margin: 2px;
+ }}
+ QPushButton:hover {{
+ background-color: {theme['hover_color']};
+ border: 1px solid {theme['border_color']};
+ }}
+ QPushButton:pressed {{
+ background-color: {theme['pressed_color']};
+ }}
+ QPushButton:disabled {{
+ background-color: {theme['primary_color']};
+ color: {theme['text_disabled_color']};
+ border: 1px solid {theme['border_color']};
+ border-radius: 6px;
+ }}
+ QCheckBox {{
+ background-color: {theme['primary_color']};
+ border-radius: 4px;
+ }}
+ QLineEdit, QTextEdit {{
+ background-color: {theme['secondary_color']};
+ border: 1px solid {theme['border_color']};
+ border-radius: 4px;
+ }}
+ QLineEdit:disabled, QLineEdit[readOnly="true"] {{
+ background-color: {theme['primary_color']};
+ color: {theme['text_disabled_color']};
+ border: 1px solid {theme['border_color']};
+ border-radius: 4px;
+ }}
+ QComboBox {{
+ background-color: {theme['secondary_color']};
+ padding: 3px;
+ border-radius: 5px;
+ }}
+ QTreeView {{
+ alternate-background-color: {theme['secondary_color']};
+ border-radius: 6px;
+ }}
+ QHeaderView::section {{
+ background-color: {theme['secondary_color']};
+ border: 1px solid {theme['primary_color']};
+ }}
+ QTabWidget::pane {{
+ border: 1px solid {theme['border_color']};
+ border-radius: 5px;
+ }}
+ QTabBar::tab {{
+ background-color: {theme['tertiary_color']};
+ border: 1px solid {theme['border_color']};
+ padding: 5px;
+ border-radius: 4px;
+ }}
+ QTabBar::tab:hover {{
+ background-color: {theme['primary_color']};
+ border: 1px solid {theme['hover_color']};
+ }}
+ QTabBar::tab:selected {{
+ background-color: {theme['primary_color']};
+ border: 1px solid {theme['hover_color']};
+ margin-top: -2px;
+ border-radius: 4px;
+ }}
+ QTabBar::tab:selected:hover {{
+ background-color: {theme['primary_color']};
+ border: 1px solid {theme['hover_color']};
+ margin-top: -2px;
+ }}
+ QTabBar::tab:!selected {{
+ margin-top: 2px;
+ }}
+ QMenuBar {{
+ background-color: {theme['secondary_color']};
+ border-radius: 5px;
+ }}
+ QMenuBar::item {{
+ background-color: transparent;
+ padding: 4px 10px;
+ border-radius: 4px;
+ }}
+ QMenuBar::item:selected {{
+ background-color: {theme['primary_color']};
+ border: 1px solid {theme['pressed_color']};
+ }}
+ QMenu {{
+ background-color: {theme['secondary_color']};
+ border: 1px solid {theme['border_color']};
+ border-radius: 6px;
+ }}
+ QMenu::item {{
+ background-color: {theme['secondary_color']};
+ }}
+ QMenu::item:selected {{
+ background-color: {theme['primary_color']};
+ }}
+ QMenu::item:disabled {{
+ background-color: {theme['primary_color']};
+ color: {theme['text_disabled_color']};
+ }}
+ QTreeWidget {{
+ background-color: {theme['tertiary_color']};
+ alternate-background-color: {theme['primary_color']};
+ }}
+ QTreeWidget::item {{
+ border: none;
+ }}
+ QTreeWidget::item:hover {{
+ background-color: {theme['main_hover']};
+ }}
+ QTreeWidget::item:selected {{
+ background-color: {theme['selected_color']};
+ }}
+ QTreeWidget::item:selected:hover {{
+ background-color: {theme['main_hover']};
+ }}
+ QScrollBar:vertical {{
+ border: 1px solid {theme['primary_color']};
+ background-color: {theme['tertiary_color']};
+ width: 16px;
+ border-radius: 6px;
+ }}
+ QScrollBar:horizontal {{
+ border: 1px solid {theme['primary_color']};
+ background-color: {theme['tertiary_color']};
+ height: 16px;
+ border-radius: 6px;
+ }}
+ QScrollBar::handle {{
+ background-color: {theme['border_color']};
+ border: 1px solid {theme['pressed_color']};
+ border-radius: 6px;
+ }}
+ QScrollBar::handle:hover {{
+ background-color: {theme['pressed_color']};
+ }}
+ QScrollBar::add-line, QScrollBar::sub-line {{
+ background-color: {theme['primary_color']};
+ }}
+ """
+
+ return stylesheet
diff --git a/autolab/core/gui/variables.py b/autolab/core/gui/variables.py
deleted file mode 100644
index 44edbaaf..00000000
--- a/autolab/core/gui/variables.py
+++ /dev/null
@@ -1,711 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Created on Mon Mar 4 14:54:41 2024
-
-@author: Jonathan
-"""
-
-import sys
-import re
-# import ast
-from typing import Any, List, Tuple, Union
-
-import numpy as np
-import pandas as pd
-from qtpy import QtCore, QtWidgets, QtGui
-
-from .GUI_utilities import setLineEditBackground
-from .icons import icons
-from ..devices import DEVICES
-from ..utilities import (str_to_array, str_to_dataframe, str_to_value,
- array_to_str, dataframe_to_str, clean_string)
-
-from .monitoring.main import Monitor
-from .slider import Slider
-
-
-# class AddVarSignal(QtCore.QObject):
-# add = QtCore.Signal(object, object)
-# def emit_add(self, name, value):
-# self.add.emit(name, value)
-
-
-# class RemoveVarSignal(QtCore.QObject):
-# remove = QtCore.Signal(object)
-# def emit_remove(self, name):
-# self.remove.emit(name)
-
-
-# class MyDict(dict):
-
-# def __init__(self):
-# self.addVarSignal = AddVarSignal()
-# self.removeVarSignal = RemoveVarSignal()
-
-# def __setitem__(self, item, value):
-# super(MyDict, self).__setitem__(item, value)
-# self.addVarSignal.emit_add(item, value)
-
-# def pop(self, item):
-# super(MyDict, self).pop(item)
-# self.removeVarSignal.emit_remove(item)
-
-
-# VARIABLES = MyDict()
-VARIABLES = {}
-
-EVAL = "$eval:"
-
-
-def update_allowed_dict() -> dict:
- global allowed_dict # needed to remove variables instead of just adding new one
- allowed_dict = {"np": np, "pd": pd}
- allowed_dict.update(DEVICES)
- allowed_dict.update(VARIABLES)
- return allowed_dict
-
-
-allowed_dict = update_allowed_dict()
-
-# TODO: replace refresh by (value)?
-# OPTIMIZE: Variable becomes closer and closer to core.elements.Variable, could envision a merge
-# TODO: refresh menu display by looking if has eval (no -> can refresh)
-# TODO add read signal to update gui (seperate class for event and use it on itemwidget creation to change setText with new value)
-class Variable():
- """ Class used to control basic variable """
-
- def __init__(self, name: str, var: Any):
- """ name: name of the variable, var: value of the variable """
- self.refresh(name, var)
-
- def refresh(self, name: str, var: Any):
- if isinstance(var, Variable):
- self.raw = var.raw
- self.value = var.value
- else:
- self.raw = var
- self.value = 'Need update' if has_eval(self.raw) else self.raw
-
- if not has_variable(self.raw):
- try: self.value = self.evaluate() # If no devices or variables found in name, can evaluate value safely
- except Exception as e: self.value = str(e)
-
- self.name = name
- self.unit = None
- self.address = lambda: name
- self.type = type(self.raw) # For slider
-
- def __call__(self, value: Any = None) -> Any:
- if value is None:
- return self.evaluate()
-
- self.refresh(self.name, value)
- return None
-
- def evaluate(self):
- if has_eval(self.raw):
- value = str(self.raw)[len(EVAL): ]
- call = eval(str(value), {}, allowed_dict)
- self.value = call
- else:
- call = self.value
-
- return call
-
- def __repr__(self) -> str:
- if isinstance(self.raw, np.ndarray):
- raw_value_str = array_to_str(self.raw, threshold=1000000, max_line_width=9000000)
- elif isinstance(self.raw, pd.DataFrame):
- raw_value_str = dataframe_to_str(self.raw, threshold=1000000)
- else:
- raw_value_str = str(self.raw)
- return raw_value_str
-
-
-def rename_variable(name, new_name):
- var = remove_variable(name)
- assert var is not None
- set_variable(new_name, var)
-
-
-def set_variable(name: str, value: Any):
- ''' Create or modify a Variable with provided name and value '''
- name = clean_string(name)
-
- if is_Variable(value):
- var = value
- var.refresh(name, value)
- else:
- var = get_variable(name)
- if var is None:
- var = Variable(name, value)
- else:
- assert is_Variable(var)
- var.refresh(name, value)
-
- VARIABLES[name] = var
- update_allowed_dict()
-
-
-def get_variable(name: str) -> Union[Variable, None]:
- ''' Return Variable with provided name if exists else None '''
- return VARIABLES.get(name)
-
-
-def remove_variable(name: str) -> Any:
- value = VARIABLES.pop(name) if name in VARIABLES else None
- update_allowed_dict()
- return value
-
-
-def remove_from_config(listVariable: List[Tuple[str, Any]]):
- for var in listVariable:
- remove_variable(var[0])
-
-
-def update_from_config(listVariable: List[Tuple[str, Any]]):
- for var in listVariable:
- set_variable(var[0], var[1])
-
-
-def convert_str_to_data(raw_value: str) -> Any:
- """ Convert data in str format to proper format """
- if not has_eval(raw_value):
- if '\t' in raw_value and '\n' in raw_value:
- try: raw_value = str_to_dataframe(raw_value)
- except: pass
- elif '[' in raw_value:
- try: raw_value = str_to_array(raw_value)
- except: pass
- else:
- try: raw_value = str_to_value(raw_value)
- except: pass
- return raw_value
-
-
-def has_variable(value: str) -> bool:
- pattern = r'[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?'
-
- for key in (list(DEVICES) + list(VARIABLES)):
- if key in [var.split('.')[0] for var in re.findall(pattern, str(value))]:
- return True
- return False
-
-
-def has_eval(value: Any) -> bool:
- """ Checks if value is a string starting with '$eval:'"""
- return True if isinstance(value, str) and value.startswith(EVAL) else False
-
-
-def is_Variable(value: Any):
- """ Returns True if value of type Variable """
- return isinstance(value, Variable)
-
-
-def eval_variable(value: Any) -> Any:
- """ Evaluate the given python string. String can contain variables,
- devices, numpy arrays and pandas dataframes."""
- if has_eval(value): value = Variable('temp', value)
-
- if is_Variable(value): return value()
- return value
-
-
-def eval_safely(value: Any) -> Any:
- """ Same as eval_variable but do not evaluate if contains devices or variables """
- if has_eval(value): value = Variable('temp', value)
-
- if is_Variable(value): return value.value
- return value
-
-
-class VariablesDialog(QtWidgets.QDialog):
-
- def __init__(self, parent: QtWidgets.QMainWindow, name: str, defaultValue: str):
-
- super().__init__(parent)
- self.setWindowTitle(name)
- self.setWindowModality(QtCore.Qt.ApplicationModal) # block GUI interaction
-
- self.variablesMenu = None
- # ORDER of creation mater to have button OK selected instead of Variables
- variablesButton = QtWidgets.QPushButton('Variables', self)
- variablesButton.clicked.connect(self.variablesButtonClicked)
-
- hbox = QtWidgets.QHBoxLayout(self)
- hbox.addStretch()
- hbox.addWidget(variablesButton)
- hbox.setContentsMargins(10,0,10,10)
-
- widget = QtWidgets.QWidget(self)
- widget.setLayout(hbox)
-
- dialog = QtWidgets.QInputDialog(self)
- dialog.setLabelText(f"Set {name} value")
- dialog.setInputMode(QtWidgets.QInputDialog.TextInput)
- dialog.setWindowFlags(dialog.windowFlags() & ~QtCore.Qt.Dialog)
-
- lineEdit = dialog.findChild(QtWidgets.QLineEdit)
- lineEdit.setMaxLength(10000000)
- dialog.setTextValue(defaultValue)
-
- layout = QtWidgets.QVBoxLayout(self)
- layout.addWidget(dialog)
- layout.addWidget(widget)
- layout.addStretch()
- layout.setSpacing(0)
- layout.setContentsMargins(0,0,0,0)
-
- self.exec_ = dialog.exec_
- self.textValue = dialog.textValue
- self.setTextValue = dialog.setTextValue
-
- def variablesButtonClicked(self):
- if self.variablesMenu is None:
- self.variablesMenu = VariablesMenu(self)
- self.variablesMenu.setWindowTitle(
- self.windowTitle()+": "+self.variablesMenu.windowTitle())
-
- self.variablesMenu.variableSignal.connect(self.toggleVariableName)
- self.variablesMenu.deviceSignal.connect(self.toggleDeviceName)
- self.variablesMenu.show()
- else:
- self.variablesMenu.refresh()
-
- def clearVariablesMenu(self):
- """ This clear the variables menu instance reference when quitted """
- self.variablesMenu = None
-
- def toggleVariableName(self, name):
- value = self.textValue()
- if is_Variable(get_variable(name)): name += '()'
-
- if value in ('0', "''"): value = ''
- if not has_eval(value): value = EVAL + value
-
- if value.endswith(name): value = value[:-len(name)]
- else: value += name
-
- if value == EVAL: value = ''
-
- self.setTextValue(value)
-
- def toggleDeviceName(self, name):
- name += '()'
- self.toggleVariableName(name)
-
- def closeEvent(self, event):
- for children in self.findChildren(QtWidgets.QWidget):
- children.deleteLater()
- super().closeEvent(event)
-
-
-class VariablesMenu(QtWidgets.QMainWindow):
-
- variableSignal = QtCore.Signal(object)
- deviceSignal = QtCore.Signal(object)
-
- def __init__(self, parent: QtWidgets.QMainWindow = None):
-
- super().__init__(parent)
- self.gui = parent
- self.setWindowTitle('Variables manager')
- if self.gui is None: self.setWindowIcon(QtGui.QIcon(icons['autolab']))
-
- self.statusBar = self.statusBar()
-
- # Main widgets creation
- self.variablesWidget = QtWidgets.QTreeWidget(self)
- self.variablesWidget.setHeaderLabels(
- ['', 'Name', 'Value', 'Evaluated value', 'Type', 'Action'])
- self.variablesWidget.setAlternatingRowColors(True)
- self.variablesWidget.setIndentation(0)
- self.variablesWidget.setStyleSheet(
- "QHeaderView::section { background-color: lightgray; }")
- header = self.variablesWidget.header()
- header.setMinimumSectionSize(20)
- header.resizeSection(0, 20)
- header.resizeSection(1, 90)
- header.resizeSection(2, 120)
- header.resizeSection(3, 120)
- header.resizeSection(4, 50)
- header.resizeSection(5, 100)
- self.variablesWidget.itemDoubleClicked.connect(self.variableActivated)
- self.variablesWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
- self.variablesWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
- self.variablesWidget.customContextMenuRequested.connect(self.rightClick)
-
- addButton = QtWidgets.QPushButton('Add')
- addButton.clicked.connect(self.addVariableAction)
-
- removeButton = QtWidgets.QPushButton('Remove')
- removeButton.clicked.connect(self.removeVariableAction)
-
- self.devicesWidget = QtWidgets.QTreeWidget(self)
- self.devicesWidget.setHeaderLabels(['Name'])
- self.devicesWidget.setAlternatingRowColors(True)
- self.devicesWidget.setIndentation(10)
- self.devicesWidget.setStyleSheet("QHeaderView::section { background-color: lightgray; }")
- self.devicesWidget.itemDoubleClicked.connect(self.deviceActivated)
-
- # Main layout creation
- layoutWindow = QtWidgets.QVBoxLayout()
- layoutTab = QtWidgets.QHBoxLayout()
- layoutWindow.addLayout(layoutTab)
-
- centralWidget = QtWidgets.QWidget()
- centralWidget.setLayout(layoutWindow)
- self.setCentralWidget(centralWidget)
-
- refreshButtonWidget = QtWidgets.QPushButton()
- refreshButtonWidget.setText('Refresh Manager')
- refreshButtonWidget.clicked.connect(self.refresh)
-
- # Main layout definition
- layoutButton = QtWidgets.QHBoxLayout()
- layoutButton.addWidget(addButton)
- layoutButton.addWidget(removeButton)
- layoutButton.addWidget(refreshButtonWidget)
- layoutButton.addStretch()
-
- frameVariables = QtWidgets.QFrame()
- layoutVariables = QtWidgets.QVBoxLayout(frameVariables)
- layoutVariables.addWidget(self.variablesWidget)
- layoutVariables.addLayout(layoutButton)
-
- frameDevices = QtWidgets.QFrame()
- layoutDevices = QtWidgets.QVBoxLayout(frameDevices)
- layoutDevices.addWidget(self.devicesWidget)
-
- tab = QtWidgets.QTabWidget(self)
- tab.addTab(frameVariables, 'Variables')
- tab.addTab(frameDevices, 'Devices')
-
- layoutTab.addWidget(tab)
-
- self.resize(550, 300)
- self.refresh()
-
- self.monitors = {}
- self.sliders = {}
- # self.timer = QtCore.QTimer(self)
- # self.timer.setInterval(400) # ms
- # self.timer.timeout.connect(self.refresh_new)
- # self.timer.start()
- # VARIABLES.removeVarSignal.remove.connect(self.removeVarSignalChanged)
- # VARIABLES.addVarSignal.add.connect(self.addVarSignalChanged)
-
- def variableActivated(self, item: QtWidgets.QTreeWidgetItem):
- self.variableSignal.emit(item.name)
-
- def rightClick(self, position: QtCore.QPoint):
- """ Provides a menu where the user right clicked to manage a variable """
- item = self.variablesWidget.itemAt(position)
- if hasattr(item, 'menu'): item.menu(position)
-
- def deviceActivated(self, item: QtWidgets.QTreeWidgetItem):
- if hasattr(item, 'name'): self.deviceSignal.emit(item.name)
-
- def removeVariableAction(self):
- for variableItem in self.variablesWidget.selectedItems():
- remove_variable(variableItem.name)
- self.removeVariableItem(variableItem)
-
- # def addVariableItem(self, name):
- # MyQTreeWidgetItem(self.variablesWidget, name, self)
-
- def removeVariableItem(self, item: QtWidgets.QTreeWidgetItem):
- index = self.variablesWidget.indexFromItem(item)
- self.variablesWidget.takeTopLevelItem(index.row())
-
- def addVariableAction(self):
- basename = 'var'
- name = basename
- names = list(VARIABLES)
-
- compt = 0
- while True:
- if name in names:
- compt += 1
- name = basename + str(compt)
- else:
- break
-
- set_variable(name, 0)
-
- variable = get_variable(name)
- MyQTreeWidgetItem(self.variablesWidget, name, variable, self) # not catched by VARIABLES signal
-
- # def addVarSignalChanged(self, key, value):
- # print('got add signal', key, value)
- # all_items = [self.variablesWidget.topLevelItem(i) for i in range(
- # self.variablesWidget.topLevelItemCount())]
-
- # for variableItem in all_items:
- # if variableItem.name == key:
- # variableItem.raw_value = get_variable(variableItem.name)
- # variableItem.refresh_rawValue()
- # variableItem.refresh_value()
- # break
- # else:
- # self.addVariableItem(key)
- # # self.refresh() # TODO: check if item exists, create if not, update if yes
-
- # def removeVarSignalChanged(self, key):
- # print('got remove signal', key)
- # all_items = [self.variablesWidget.topLevelItem(i) for i in range(
- # self.variablesWidget.topLevelItemCount())]
-
- # for variableItem in all_items:
- # if variableItem.name == key:
- # self.removeVariableItem(variableItem)
-
- # # self.refresh() # TODO: check if exists, remove if yes
-
- def refresh(self):
- self.variablesWidget.clear()
- for var_name in VARIABLES:
- variable = get_variable(var_name)
- MyQTreeWidgetItem(self.variablesWidget, var_name, variable, self)
-
- self.devicesWidget.clear()
- for device_name in DEVICES:
- device = DEVICES[device_name]
- deviceItem = QtWidgets.QTreeWidgetItem(
- self.devicesWidget, [device_name])
- deviceItem.setBackground(0, QtGui.QColor('#9EB7F5')) # blue
- deviceItem.setExpanded(True)
- for elements in device.get_structure():
- deviceItem2 = QtWidgets.QTreeWidgetItem(
- deviceItem, [elements[0]])
- deviceItem2.name = elements[0]
-
- def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
- """ Modify the message displayed in the status bar and add error message to logger """
- self.statusBar.showMessage(message, timeout)
- if not stdout: print(message, file=sys.stderr)
-
- def closeEvent(self, event):
- # self.timer.stop()
- if hasattr(self.gui, 'clearVariablesMenu'):
- self.gui.clearVariablesMenu()
-
- for monitor in list(self.monitors.values()):
- monitor.close()
-
- for slider in list(self.sliders.values()):
- slider.close()
-
- for children in self.findChildren(QtWidgets.QWidget):
- children.deleteLater()
-
- super().closeEvent(event)
-
- if self.gui is None:
- QtWidgets.QApplication.quit() # close the variable app
-
-class MyQTreeWidgetItem(QtWidgets.QTreeWidgetItem):
-
- def __init__(self, itemParent, name, variable, gui):
-
- super().__init__(itemParent, ['', name])
-
- self.itemParent = itemParent
- self.gui = gui
- self.name = name
- self.variable = variable
-
- nameWidget = QtWidgets.QLineEdit()
- nameWidget.setText(name)
- nameWidget.setAlignment(QtCore.Qt.AlignCenter)
- nameWidget.returnPressed.connect(self.renameVariable)
- nameWidget.textEdited.connect(lambda: setLineEditBackground(
- nameWidget, 'edited'))
- setLineEditBackground(nameWidget, 'synced')
- self.gui.variablesWidget.setItemWidget(self, 1, nameWidget)
- self.nameWidget = nameWidget
-
- rawValueWidget = QtWidgets.QLineEdit()
- rawValueWidget.setMaxLength(10000000)
- rawValueWidget.setAlignment(QtCore.Qt.AlignCenter)
- rawValueWidget.returnPressed.connect(self.changeRawValue)
- rawValueWidget.textEdited.connect(lambda: setLineEditBackground(
- rawValueWidget, 'edited'))
- self.gui.variablesWidget.setItemWidget(self, 2, rawValueWidget)
- self.rawValueWidget = rawValueWidget
-
- valueWidget = QtWidgets.QLineEdit()
- valueWidget.setMaxLength(10000000)
- valueWidget.setReadOnly(True)
- valueWidget.setStyleSheet(
- "QLineEdit {border: 1px solid #a4a4a4; background-color: #f4f4f4}")
- valueWidget.setAlignment(QtCore.Qt.AlignCenter)
- self.gui.variablesWidget.setItemWidget(self, 3, valueWidget)
- self.valueWidget = valueWidget
-
- typeWidget = QtWidgets.QLabel()
- typeWidget.setAlignment(QtCore.Qt.AlignCenter)
- self.gui.variablesWidget.setItemWidget(self, 4, typeWidget)
- self.typeWidget = typeWidget
-
- self.actionButtonWidget = None
-
- self.refresh_rawValue()
- self.refresh_value()
-
- def menu(self, position: QtCore.QPoint):
- """ This function provides the menu when the user right click on an item """
- menu = QtWidgets.QMenu()
- monitoringAction = menu.addAction("Start monitoring")
- monitoringAction.setIcon(QtGui.QIcon(icons['monitor']))
- monitoringAction.setEnabled(has_eval(self.variable.raw) or isinstance(
- self.variable.value, (int, float, np.ndarray, pd.DataFrame)))
-
- menu.addSeparator()
- sliderAction = menu.addAction("Create a slider")
- sliderAction.setIcon(QtGui.QIcon(icons['slider']))
- sliderAction.setEnabled(self.variable.type in (int, float))
-
- choice = menu.exec_(self.gui.variablesWidget.viewport().mapToGlobal(position))
- if choice == monitoringAction: self.openMonitor()
- elif choice == sliderAction: self.openSlider()
-
- def openMonitor(self):
- """ This function open the monitor associated to this variable. """
- # If the monitor is not already running, create one
- if id(self) not in self.gui.monitors:
- self.gui.monitors[id(self)] = Monitor(self)
- self.gui.monitors[id(self)].show()
- # If the monitor is already running, just make as the front window
- else:
- monitor = self.gui.monitors[id(self)]
- monitor.setWindowState(
- monitor.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
- monitor.activateWindow()
-
- def openSlider(self):
- """ This function open the slider associated to this variable. """
- # If the slider is not already running, create one
- if id(self) not in self.gui.sliders:
- self.gui.sliders[id(self)] = Slider(self.variable, self)
- self.gui.sliders[id(self)].show()
- # If the slider is already running, just make as the front window
- else:
- slider = self.gui.sliders[id(self)]
- slider.setWindowState(
- slider.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
- slider.activateWindow()
-
- def clearMonitor(self):
- """ This clear monitor instances reference when quitted """
- if id(self) in self.gui.monitors:
- self.gui.monitors.pop(id(self))
-
- def clearSlider(self):
- """ This clear the slider instances reference when quitted """
- if id(self) in self.gui.sliders:
- self.gui.sliders.pop(id(self))
-
- def renameVariable(self):
- new_name = self.nameWidget.text()
- if new_name == self.name:
- setLineEditBackground(self.nameWidget, 'synced')
- return None
-
- if new_name in VARIABLES:
- self.gui.setStatus(
- f"Error: {new_name} already exist!", 10000, False)
- return None
-
- for character in r'$*."/\[]:;|, -(){}^=':
- new_name = new_name.replace(character, '')
-
- try:
- rename_variable(self.name, new_name)
- except Exception as e:
- self.gui.setStatus(f'Error: {e}', 10000, False)
- else:
- self.name = new_name
- new_name = self.nameWidget.setText(self.name)
- setLineEditBackground(self.nameWidget, 'synced')
- self.gui.setStatus('')
-
- def refresh_rawValue(self):
- raw_value = self.variable.raw
-
- if isinstance(raw_value, np.ndarray):
- raw_value_str = array_to_str(raw_value)
- elif isinstance(raw_value, pd.DataFrame):
- raw_value_str = dataframe_to_str(raw_value)
- else:
- raw_value_str = str(raw_value)
-
- self.rawValueWidget.setText(raw_value_str)
- setLineEditBackground(self.rawValueWidget, 'synced')
-
- if has_variable(self.variable): # OPTIMIZE: use hide and show instead but doesn't hide on instantiation
- if self.actionButtonWidget is None:
- actionButtonWidget = QtWidgets.QPushButton()
- actionButtonWidget.setText('Update value')
- actionButtonWidget.setMinimumSize(0, 23)
- actionButtonWidget.setMaximumSize(85, 23)
- actionButtonWidget.clicked.connect(self.convertVariableClicked)
- self.gui.variablesWidget.setItemWidget(self, 5, actionButtonWidget)
- self.actionButtonWidget = actionButtonWidget
- else:
- self.gui.variablesWidget.removeItemWidget(self, 5)
- self.actionButtonWidget = None
-
- def refresh_value(self):
- value = self.variable.value
-
- if isinstance(value, np.ndarray):
- value_str = array_to_str(value)
- elif isinstance(value, pd.DataFrame):
- value_str = dataframe_to_str(value)
- else:
- value_str = str(value)
-
- self.valueWidget.setText(value_str)
- self.typeWidget.setText(str(type(value).__name__))
-
- def changeRawValue(self):
- name = self.name
- raw_value = self.rawValueWidget.text()
- try:
- if not has_eval(raw_value):
- raw_value = convert_str_to_data(raw_value)
- else:
- # get all variables
- pattern1 = r'[a-zA-Z][a-zA-Z0-9._]*'
- matches1 = re.findall(pattern1, raw_value)
- # get variables not unclosed by ' or " (gives bad name so needs to check with all variables)
- pattern2 = r'(? str:
- ''' Returns a list of all the drivers with categories by sections (autolab drivers, local drivers) '''
- drivers.update_drivers_paths()
+def _list_drivers(_print: bool = True) -> str:
+ ''' Returns a list of all the drivers with categories by sections
+ (autolab drivers, local drivers) '''
+ update_drivers_paths()
s = '\n'
- s += f'{len(drivers.DRIVERS_PATHS)} drivers found\n\n'
+ s += f'{len(DRIVERS_PATHS)} drivers found\n\n'
- for i, (source_name, source) in enumerate(paths.DRIVER_SOURCES.items()):
- sub_driver_list = sorted([key for key, val in drivers.DRIVERS_PATHS.items() if val['source']==source_name])
+ for i, (source_name, source) in enumerate(DRIVER_SOURCES.items()):
+ sub_driver_list = sorted([key for key, val in DRIVERS_PATHS.items(
+ ) if val['source'] == source_name])
s += f'Drivers in {source}:\n'
if len(sub_driver_list) > 0:
- txt_list = [[f' - {driver_name}',
- f'({drivers.get_driver_category(driver_name)})']
+ txt_list = [[f' - {driver_name}',
+ f'({get_driver_category(driver_name)})']
for driver_name in sub_driver_list]
- s += utilities.two_columns(txt_list) + '\n\n'
+ s += two_columns(txt_list) + '\n\n'
else:
- if (i + 1) == len(paths.DRIVER_SOURCES):
- s += ' \n\n'
+ if (i + 1) == len(DRIVER_SOURCES):
+ s += ' \n\n'
else:
- s += ' (or overwritten)\n\n'
+ s += ' (or overwritten)\n\n'
if _print:
print(s)
@@ -38,19 +43,20 @@ def list_drivers(_print: bool = True) -> str:
return s
-def list_devices(_print: bool = True) -> str:
- ''' Returns a list of all the devices and their associated drivers from devices_config.ini '''
+def _list_devices(_print: bool = True) -> str:
+ ''' Returns a list of all the devices and their associated drivers
+ from devices_config.ini '''
# Gather local config informations
- devices_names = devices.list_devices()
- devices_names_loaded = devices.list_loaded_devices()
+ devices_names = list_devices()
+ devices_names_loaded = list_loaded_devices()
# Build infos str for devices
s = '\n'
s += f'{len(devices_names)} devices found\n\n'
- txt_list = [[f' - {name} ' + ('[loaded]' if name in devices_names_loaded else ''),
- f'({config.get_device_config(name)["driver"]})']
- for name in devices_names]
- s += utilities.two_columns(txt_list) + '\n'
+ txt_list = [
+ [f' - {name} ' + ('[loaded]' if name in devices_names_loaded else ''),
+ f'({get_device_config(name)["driver"]})'] for name in devices_names]
+ s += two_columns(txt_list) + '\n'
if _print:
print(s)
@@ -59,10 +65,11 @@ def list_devices(_print: bool = True) -> str:
def infos(_print: bool = True) -> str:
- ''' Returns a list of all the drivers and all the devices, along with their associated drivers from devices_config.ini '''
+ ''' Returns a list of all the drivers and all the devices,
+ along with their associated drivers from devices_config.ini '''
s = ''
- s += list_drivers(_print=False)
- s += list_devices(_print=False)
+ s += _list_drivers(_print=False)
+ s += _list_devices(_print=False)
if _print:
print(s)
@@ -76,31 +83,31 @@ def infos(_print: bool = True) -> str:
def config_help(driver_name: str, _print: bool = True, _parser: bool = False) -> str:
''' Display the help of a particular driver (connection types, modules, ...) '''
try:
- driver_name = devices.get_final_device_config(driver_name)["driver"]
+ driver_name = get_final_device_config(driver_name)["driver"]
except:
pass
# Load list of all parameters
try:
- driver_lib = drivers.load_driver_lib(driver_name)
+ driver_lib = load_driver_lib(driver_name)
except Exception as e:
print(f"Can't load {driver_name}: {e}", file=sys.stderr)
return None
params = {}
params['driver'] = driver_name
params['connection'] = {}
- for conn in drivers.get_connection_names(driver_lib):
- params['connection'][conn] = drivers.get_class_args(
- drivers.get_connection_class(driver_lib, conn))
- params['other'] = drivers.get_class_args(drivers.get_driver_class(driver_lib))
- if hasattr(drivers.get_driver_class(driver_lib), 'slot_config'):
- params['other']['slot1'] = f'{drivers.get_driver_class(driver_lib).slot_config}'
+ for conn in get_connection_names(driver_lib):
+ params['connection'][conn] = get_class_args(
+ get_connection_class(driver_lib, conn))
+ params['other'] = get_class_args(get_driver_class(driver_lib))
+ if hasattr(get_driver_class(driver_lib), 'slot_config'):
+ params['other']['slot1'] = f'{get_driver_class(driver_lib).slot_config}'
params['other']['slot1_name'] = 'my_'
mess = '\n'
# Name and category if available
- submess = f'Driver "{driver_name}" ({drivers.get_driver_category(driver_name)})'
- mess += utilities.emphasize(submess, sign='=') + '\n'
+ submess = f'Driver "{driver_name}" ({get_driver_category(driver_name)})'
+ mess += emphasize(submess, sign='=') + '\n'
# Connections types
c_option=' (-C option)' if _parser else ''
@@ -110,30 +117,30 @@ def config_help(driver_name: str, _print: bool = True, _parser: bool = False) ->
mess += '\n'
# Modules
- if hasattr(drivers.get_driver_class(driver_lib), 'slot_config'):
+ if hasattr(get_driver_class(driver_lib), 'slot_config'):
mess += 'Available modules:\n'
- modules = drivers.get_module_names(driver_lib)
+ modules = get_module_names(driver_lib)
for module in modules:
- moduleClass = drivers.get_module_class(driver_lib, module)
+ moduleClass = get_module_class(driver_lib, module)
mess += f' - {module}'
if hasattr(moduleClass, 'category'): mess += f' ({moduleClass.category})'
mess += '\n'
mess += '\n'
# Example of a devices_config.ini section
- mess += '\n' + utilities.underline(
+ mess += '\n' + underline(
'Saving a Device configuration in devices_config.ini:') + '\n'
for conn in params['connection']:
- mess += f"\n [my_{params['driver']}]\n"
- mess += f" driver = {params['driver']}\n"
- mess += f" connection = {conn}\n"
+ mess += f"\n[my_{params['driver']}]\n"
+ mess += f"driver = {params['driver']}\n"
+ mess += f"connection = {conn}\n"
for arg, value in params['connection'][conn].items():
- mess += f" {arg} = {value}\n"
+ mess += f"{arg} = {value}\n"
for arg, value in params['other'].items():
- mess += f" {arg} = {value}\n"
+ mess += f"{arg} = {value}\n"
# Example of get_driver
- mess += '\n' + utilities.underline('Loading a Driver:') + '\n\n'
+ mess += '\n\n' + underline('Loading a Driver:') + '\n\n'
for conn in params['connection']:
if not _parser:
args_str = f"'{params['driver']}', connection='{conn}'"
@@ -144,7 +151,7 @@ def config_help(driver_name: str, _print: bool = True, _parser: bool = False) ->
args_str += f", {arg}='{value}'"
else:
args_str += f", {arg}={value}"
- mess += f" a = autolab.get_driver({args_str})\n"
+ mess += f"a = autolab.get_driver({args_str})\n"
else:
args_str = f"-D {params['driver']} -C {conn} "
for arg,value in params['connection'][conn].items():
@@ -153,15 +160,15 @@ def config_help(driver_name: str, _print: bool = True, _parser: bool = False) ->
if len(params['other']) > 0: args_str += '-O '
for arg,value in params['other'].items():
args_str += f"{arg}={value} "
- mess += f" autolab driver {args_str} -m method(value) \n"
+ mess += f"autolab driver {args_str} -m method(value) \n"
# Example of get_device
- mess += '\n\n' + utilities.underline(
+ mess += '\n\n' + underline(
'Loading a Device configured in devices_config.ini:') + '\n\n'
if not _parser:
- mess += f" a = autolab.get_device('my_{params['driver']}')"
+ mess += f"a = autolab.get_device('my_{params['driver']}')"
else:
- mess += f" autolab device -D my_{params['driver']} -e element -v value \n"
+ mess += f"autolab device -D my_{params['driver']} -e element -v value \n"
if _print:
print(mess)
diff --git a/autolab/core/paths.py b/autolab/core/paths.py
index 8725c115..905101e1 100644
--- a/autolab/core/paths.py
+++ b/autolab/core/paths.py
@@ -7,22 +7,21 @@
import os
-
AUTOLAB_FOLDER = os.path.dirname(os.path.dirname(__file__))
VERSION = os.path.join(AUTOLAB_FOLDER, 'version.txt')
+LAST_FOLDER = os.path.expanduser('~')
USER_FOLDER = os.path.join(os.path.expanduser('~'), 'autolab')
-USER_LAST_CUSTOM_FOLDER = os.path.expanduser('~')
DEVICES_CONFIG = os.path.join(USER_FOLDER, 'devices_config.ini')
AUTOLAB_CONFIG = os.path.join(USER_FOLDER, 'autolab_config.ini')
PLOTTER_CONFIG = os.path.join(USER_FOLDER, 'plotter_config.ini')
HISTORY_CONFIG = os.path.join(USER_FOLDER, '.history_config.txt')
# Drivers locations
-DRIVERS = os.path.join(USER_FOLDER,'drivers')
+DRIVERS = os.path.join(USER_FOLDER, 'drivers')
DRIVER_LEGACY = {'official': os.path.join(AUTOLAB_FOLDER, 'drivers'),
- 'local': os.path.join(USER_FOLDER, 'local_drivers')}
+ 'local': os.path.join(USER_FOLDER, 'local_drivers')}
# can add paths in autolab_config.ini [extra_driver_path]
DRIVER_SOURCES = {'official': os.path.join(DRIVERS, 'official'),
'local': os.path.join(DRIVERS, 'local')}
@@ -31,3 +30,12 @@
# can add paths in autolab_config.ini [extra_driver_url_repo]
# format is {'path to install': 'url to download'}
DRIVER_REPOSITORY = {DRIVER_SOURCES['official']: 'https://github.com/autolab-project/autolab-drivers'}
+
+PATHS = {'autolab_folder': AUTOLAB_FOLDER, 'version': VERSION,
+ 'user_folder': USER_FOLDER, 'drivers': DRIVERS,
+ 'devices_config': DEVICES_CONFIG, 'autolab_config': AUTOLAB_CONFIG,
+ 'plotter_config': PLOTTER_CONFIG, 'history_config': HISTORY_CONFIG,
+ 'last_folder': LAST_FOLDER}
+
+# Storage of the drivers paths
+DRIVERS_PATHS = {}
diff --git a/autolab/core/repository.py b/autolab/core/repository.py
index 10377269..793f3123 100644
--- a/autolab/core/repository.py
+++ b/autolab/core/repository.py
@@ -14,8 +14,8 @@
import json
from typing import Union, Tuple
-from . import paths
-from . import drivers
+from .paths import DRIVER_SOURCES, DRIVER_REPOSITORY
+from .drivers import update_drivers_paths
from .utilities import input_wrap
from .gitdir import download
@@ -117,25 +117,21 @@ def _copy_move(temp_unzip_repo, filename, output_dir):
def _check_empty_driver_folder():
- if not os.listdir(paths.DRIVER_SOURCES['official']):
- print(f"No drivers found in {paths.DRIVER_SOURCES['official']}")
+ if not os.listdir(DRIVER_SOURCES['official']):
+ print(f"No drivers found in {DRIVER_SOURCES['official']}")
install_drivers()
def install_drivers(*repo_url: Union[None, str, Tuple[str, str]],
- skip_input=False, experimental_feature=False):
+ skip_input=False):
""" Ask if want to install drivers from repo url.
repo_url: can be url or tuple ('path to install', 'url to download').
If no argument passed, download official drivers to official driver folder.
If only url given, use official driver folder.
Also install mandatory drivers (system, dummy, plotter) from official repo."""
- if experimental_feature:
- _install_drivers_custom()
- return None
-
# Download mandatory drivers
- official_folder = paths.DRIVER_SOURCES['official']
- official_url = paths.DRIVER_REPOSITORY[official_folder]
+ official_folder = DRIVER_SOURCES['official']
+ official_url = DRIVER_REPOSITORY[official_folder]
mandatory_drivers = ['system', 'dummy', 'plotter']
for driver in mandatory_drivers:
@@ -149,7 +145,7 @@ def install_drivers(*repo_url: Union[None, str, Tuple[str, str]],
# create list of tuple with tuple being ('path to install', 'url to download')
if len(repo_url) == 0:
- list_repo_tuple = list(paths.DRIVER_REPOSITORY.items()) # This variable can be modified in autolab_config.ini
+ list_repo_tuple = list(DRIVER_REPOSITORY.items()) # This variable can be modified in autolab_config.ini
else:
list_repo_tuple = list(repo_url)
for i, repo_url_tmp in enumerate(list_repo_tuple):
@@ -185,7 +181,7 @@ def install_drivers(*repo_url: Union[None, str, Tuple[str, str]],
os.rmdir(temp_repo_folder)
# Update available drivers
- drivers.update_drivers_paths()
+ update_drivers_paths()
# =============================================================================
@@ -234,151 +230,3 @@ def _download_driver(url, driver_name, output_dir, _print=True):
print(e, file=sys.stderr)
else:
return e
-
-
-def _install_drivers_custom(_print=True, parent=None):
- """ Ask the user which driver to install from the official autolab driver github repo.
- If qtpy is install, open a GUI to select the driver.
- Else, prompt the user to install individual drivers. """
- official_folder = paths.DRIVER_SOURCES['official']
- official_url = paths.DRIVER_REPOSITORY[official_folder]
-
- try:
- list_driver = _get_drivers_list_from_github(official_url)
- except:
- print(f'Cannot access {official_url}, skip installation')
- return None
-
- try:
- from qtpy import QtWidgets, QtGui
- except:
- print("No qtpy installed. Using the console to install drivers instead")
-
- if _print:
- print(f"Drivers will be downloaded to {official_folder}")
- for driver_name in list_driver:
- ans = input(f'Download {driver_name}? [default:yes] > ') # didn't use input_wrap because don't want to say yes to download all drivers
- if ans.strip().lower() == 'stop':
- break
- if ans.strip().lower() != 'no':
- _download_driver(official_url, driver_name, official_folder, _print=_print)
- else:
-
- class DriverInstaller(QtWidgets.QMainWindow):
-
- def __init__(self, url, list_driver, OUTPUT_DIR, parent=None):
- """ GUI to select which driver to install from the official github repo """
-
- self.gui = parent
- self.url = url
- self.list_driver = list_driver
- self.OUTPUT_DIR = OUTPUT_DIR
-
- super().__init__(parent)
-
- self.setWindowTitle("Autolab - Driver Installer")
- self.setFocus()
- self.activateWindow()
-
- self.statusBar = self.statusBar()
-
- centralWidget = QtWidgets.QWidget()
- self.setCentralWidget(centralWidget)
-
- # OFFICIAL DRIVERS
- formLayout = QtWidgets.QFormLayout()
-
- self.masterCheckBox = QtWidgets.QCheckBox(f"From {paths.DRIVER_REPOSITORY[paths.DRIVER_SOURCES['official']]}:")
- self.masterCheckBox.setChecked(False)
- self.masterCheckBox.stateChanged.connect(self.masterCheckBoxChanged)
- formLayout.addRow(self.masterCheckBox)
-
- # Init table size
- sti = QtGui.QStandardItemModel()
- for i in range(len(self.list_driver)):
- sti.appendRow([QtGui.QStandardItem(str())])
-
- # Create table
- tab = QtWidgets.QTableView()
- tab.setModel(sti)
- tab.verticalHeader().setVisible(False)
- tab.horizontalHeader().setVisible(False)
- tab.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
- tab.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
- tab.setAlternatingRowColors(True)
- tab.horizontalHeader().setSectionResizeMode(
- 0, QtWidgets.QHeaderView.ResizeToContents)
- tab.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
- QtWidgets.QSizePolicy.Expanding)
- tab.setSizeAdjustPolicy(tab.AdjustToContents)
-
- # Init checkBox
- self.list_checkBox = []
- for i, driver_name in enumerate(self.list_driver):
- checkBox = QtWidgets.QCheckBox(f"{driver_name}")
- checkBox.setChecked(False)
- self.list_checkBox.append(checkBox)
- tab.setIndexWidget(sti.index(i, 0), checkBox)
-
- formLayout.addRow(QtWidgets.QLabel(""), tab)
-
- download_pushButton = QtWidgets.QPushButton()
- download_pushButton.clicked.connect(self.installListDriver)
- download_pushButton.setText("Download")
- formLayout.addRow(download_pushButton)
-
- centralWidget.setLayout(formLayout)
-
- def masterCheckBoxChanged(self):
- """ Checked all the checkBox related to the official github repo """
- state = self.masterCheckBox.isChecked()
- for checkBox in self.list_checkBox:
- checkBox.setChecked(state)
-
- def installListDriver(self):
- """ Install all the drivers for which the corresponding checkBox has been checked """
- list_bool = [
- checkBox.isChecked() for checkBox in self.list_checkBox]
- list_driver_to_download = [
- driver_name for (driver_name, driver_bool) in zip(
- self.list_driver, list_bool) if driver_bool]
-
- if all(list_bool): # Better for all drivers
- install_drivers(skip_input=True, experimental_feature=False)
- self.close()
- elif any(list_bool): # Better for couple drivers
- for driver_name in list_driver_to_download:
- if _print:
- print(f"Downloading {driver_name}")
- # self.setStatus(f"Downloading {driver_name}", 5000) # OPTIMIZE: currently thread blocked by installer so don't show anything until the end
- e = _download_driver(self.url, driver_name,
- self.OUTPUT_DIR, _print=False)
- if e is not None:
- print(e, file=sys.stderr)
- # self.setStatus(e, 10000, False)
- self.setStatus('Finished!', 5000)
-
- def closeEvent(self, event):
- """ This function does some steps before the window is really killed """
- super().closeEvent(event)
-
- if self.gui is None:
- QtWidgets.QApplication.quit() # close the app
-
- def setStatus(self, message: str, timeout: int = 0, stdout: bool = True):
- """ Modify the message displayed in the status bar and add error message to logger """
- self.statusBar.showMessage(message, timeout)
- if not stdout: print(message, file=sys.stderr)
-
- if parent is None:
- if _print: print("Open driver installer")
- app = QtWidgets.QApplication.instance()
- if app is None: app = QtWidgets.QApplication([])
-
- driverInstaller = DriverInstaller(
- official_url, list_driver, official_folder, parent=parent)
- driverInstaller.show()
- if parent is None: app.exec()
-
- # Update available drivers
- drivers.update_drivers_paths()
diff --git a/autolab/core/server.py b/autolab/core/server.py
index 9460c967..76f93d09 100644
--- a/autolab/core/server.py
+++ b/autolab/core/server.py
@@ -3,10 +3,12 @@
import socket
import pickle
import threading
-from . import config, devices
import datetime as dt
from functools import partial
+from .config import get_server_config
+from .devices import get_devices_status, get_device
+
class Driver_SOCKET():
@@ -110,14 +112,14 @@ def process_command(self, command):
if command == 'CLOSE_CONNECTION':
self.stop_flag.set()
elif command == 'DEVICES_STATUS?':
- return self.write(devices.get_devices_status())
+ return self.write(get_devices_status())
else:
if command['command'] == 'get_device_model':
device_name = command['device_name']
- structure = devices.get_device(device_name).get_structure()
+ structure = get_device(device_name).get_structure()
self.write(structure)
elif command['command'] == 'request':
- devices.get_devices(device_name).get_by_adress(command['element_adress'])
+ get_device(device_name).get_by_adress(command['element_adress'])
# element_address --> my_yenista::submodule::wavelength
wavelength()
@@ -148,7 +150,7 @@ def __init__(self, port=None):
self.active_connection_thread = None
# Load server config in autolab_config.ini
- server_config = config.get_server_config()
+ server_config = get_server_config()
if not port: port = int(server_config['port'])
self.port = port
diff --git a/autolab/core/utilities.py b/autolab/core/utilities.py
index eb9048fe..25492aaa 100644
--- a/autolab/core/utilities.py
+++ b/autolab/core/utilities.py
@@ -4,7 +4,7 @@
@author: qchat
"""
-from typing import Any, List
+from typing import Any, List, Tuple
import re
import ast
from io import StringIO
@@ -15,7 +15,7 @@
import pandas as pd
-SUPPORTED_EXTENSION = "Text Files (*.txt);; Supported text Files (*.txt;*.csv;*.dat);; All Files (*)"
+SUPPORTED_EXTENSION = "Text Files (*.txt);; Supported text Files (*.txt;*.csv;*.dat);; Any Files (*)"
def emphasize(txt: str, sign: str = '-') -> str:
@@ -81,12 +81,33 @@ def str_to_value(s: str) -> Any:
return s
+def str_to_tuple(s: str) -> Tuple[List[str], int]:
+ ''' Convert string to Tuple[List[str], int] '''
+ e = "Input string does not match the required format Tuple[List[str], int]"
+ try:
+ result = ast.literal_eval(s)
+ e = f"{result} does not match the required format Tuple[List[str], int]"
+ assert (isinstance(result, (tuple, list))
+ and len(result) == 2
+ and isinstance(result[0], (list, tuple))
+ and isinstance(result[1], int)), e
+ result = ([str(res) for res in result[0]], result[1])
+ return result
+ except Exception:
+ raise Exception(e)
+
def create_array(value: Any) -> np.ndarray:
- ''' Format an int, float, list or numpy array to a numpy array with minimal one dimension '''
- # ndim=1 to avoid having float if 0D
- array = np.array(value, ndmin=1, dtype=float) # check validity of array
- array = np.array(value, ndmin=1, copy=False) # keep original dtype
- return array
+ ''' Format an int, float, list or numpy array to a numpy array with at least
+ one dimension '''
+ # check validity of array, raise error if dtype not int or float
+ np.array(value, ndmin=1, dtype=float)
+ # Convert to ndarray and keep original dtype
+ value = np.asarray(value)
+
+ # want ndim >= 1 to avoid having float if 0D
+ while value.ndim < 1:
+ value = np.expand_dims(value, axis=0)
+ return value
def str_to_array(s: str) -> np.ndarray:
@@ -106,9 +127,11 @@ def array_to_str(value: Any, threshold: int = None, max_line_width: int = None)
def str_to_dataframe(s: str) -> pd.DataFrame:
''' Convert a string to a pandas DataFrame '''
- value_io = StringIO(s)
- # TODO: find sep (use \t to be compatible with excel but not nice to write by hand)
- df = pd.read_csv(value_io, sep="\t")
+ if s == '\r\n': # empty
+ df = pd.DataFrame()
+ else:
+ value_io = StringIO(s)
+ df = pd.read_csv(value_io, sep="\t")
return df
@@ -118,7 +141,32 @@ def dataframe_to_str(value: pd.DataFrame, threshold=1000) -> str:
return pd.DataFrame(value).head(threshold).to_csv(index=False, sep="\t") # can't display full data to QLineEdit, need to truncate (numpy does the same)
-def openFile(filename: str):
+def str_to_data(s: str) -> Any:
+ """ Convert str to data with special format for ndarray and dataframe """
+ if '\t' in s and '\n' in s:
+ try: s = str_to_dataframe(s)
+ except: pass
+ elif '[' in s:
+ try: s = str_to_array(s)
+ except: pass
+ else:
+ try: s = str_to_value(s)
+ except: pass
+ return s
+
+
+def data_to_str(value: Any) -> str:
+ """ Convert data to str with special format for ndarray and dataframe """
+ if isinstance(value, np.ndarray):
+ raw_value_str = array_to_str(value, threshold=1000000, max_line_width=9000000)
+ elif isinstance(value, pd.DataFrame):
+ raw_value_str = dataframe_to_str(value, threshold=1000000)
+ else:
+ raw_value_str = str(value)
+ return raw_value_str
+
+
+def open_file(filename: str):
''' Opens a file using the platform specific command '''
system = platform.system()
if system == 'Windows': os.startfile(filename)
@@ -126,7 +174,7 @@ def openFile(filename: str):
elif system == 'Darwin': os.system(f'open "{filename}"')
-def formatData(data: Any) -> pd.DataFrame:
+def data_to_dataframe(data: Any) -> pd.DataFrame:
""" Format data to DataFrame """
try: data = pd.DataFrame(data)
except ValueError: data = pd.DataFrame([data])
@@ -140,12 +188,12 @@ def formatData(data: Any) -> pd.DataFrame:
pass # OPTIMIZE: This happens when there is identical column name
if len(data) != 0:
- assert not data.isnull().values.all(), f"Datatype '{data_type}' not supported"
+ assert not data.isnull().values.all(), f"Datatype '{data_type}' is not supported"
if data.iloc[-1].isnull().values.all(): # if last line is full of nan, remove it
data = data[:-1]
if data.shape[1] == 1:
- data.rename(columns = {'0':'1'}, inplace=True)
+ data.rename(columns = {'0': '1'}, inplace=True)
data.insert(0, "0", range(data.shape[0]))
return data
diff --git a/autolab/core/variables.py b/autolab/core/variables.py
new file mode 100644
index 00000000..a73fcac4
--- /dev/null
+++ b/autolab/core/variables.py
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Mar 4 14:54:41 2024
+
+@author: Jonathan
+"""
+
+import re
+from typing import Any, List, Tuple
+
+import numpy as np
+import pandas as pd
+
+from .devices import DEVICES
+from .utilities import clean_string
+
+
+# class AddVarSignal(QtCore.QObject):
+# add = QtCore.Signal(object, object)
+# def emit_add(self, name, value):
+# self.add.emit(name, value)
+
+
+# class RemoveVarSignal(QtCore.QObject):
+# remove = QtCore.Signal(object)
+# def emit_remove(self, name):
+# self.remove.emit(name)
+
+
+# class MyDict(dict):
+
+# def __init__(self):
+# self.addVarSignal = AddVarSignal()
+# self.removeVarSignal = RemoveVarSignal()
+
+# def __setitem__(self, item, value):
+# super(MyDict, self).__setitem__(item, value)
+# self.addVarSignal.emit_add(item, value)
+
+# def pop(self, item):
+# super(MyDict, self).pop(item)
+# self.removeVarSignal.emit_remove(item)
+
+
+# VARIABLES = MyDict()
+VARIABLES = {}
+
+EVAL = "$eval:"
+
+
+def update_allowed_dict() -> dict:
+ global allowed_dict # needed to remove variables instead of just adding new one
+ allowed_dict = {"np": np, "pd": pd}
+ allowed_dict.update(DEVICES)
+ allowed_dict.update(VARIABLES)
+ return allowed_dict
+
+
+allowed_dict = update_allowed_dict()
+
+# OPTIMIZE: Variable becomes closer and closer to core.elements.Variable, could envision a merge
+# TODO: refresh menu display by looking if has eval (no -> can refresh)
+# TODO add read signal to update gui (separate class for event and use it on itemwidget creation to change setText with new value)
+class Variable():
+ """ Class used to control basic variable """
+
+ raw: Any
+ value: Any
+
+ def __init__(self, name: str, var: Any):
+ """ name: name of the variable, var: value of the variable """
+ self.unit = None
+ self.writable = True
+ self.readable = True
+ self._rename(name)
+ self.write_function(var)
+
+ def _rename(self, new_name: str):
+ self.name = new_name
+ self.address = lambda: new_name
+
+ def write_function(self, var: Any):
+ if isinstance(var, Variable):
+ self.raw = var.raw
+ self.value = var.value
+ else:
+ self.raw = var
+ self.value = 'Need update' if has_eval(self.raw) else self.raw
+
+ # If no devices or variables with char '(' found in raw, can evaluate value safely
+ if not has_variable(self.raw) or '(' not in self.raw:
+ try: self.value = self.read_function()
+ except Exception as e: self.value = str(e)
+
+ self.type = type(self.raw) # For slider
+
+ def read_function(self):
+ if has_eval(self.raw):
+ value = str(self.raw)[len(EVAL): ]
+ call = eval(str(value), {}, allowed_dict)
+ self.value = call
+ else:
+ call = self.value
+
+ return call
+
+ def __call__(self, value: Any = None) -> Any:
+ if value is None:
+ return self.read_function()
+
+ self.write_function(value)
+ return None
+
+
+def list_variables() -> List[str]:
+ ''' Returns a list of Variables '''
+ return list(VARIABLES)
+
+
+def rename_variable(name: str, new_name: str):
+ ''' Rename an existing Variable '''
+ new_name = clean_string(new_name)
+ var = VARIABLES.pop(name)
+ VARIABLES[new_name] = var
+ var._rename(new_name)
+ update_allowed_dict()
+
+
+def set_variable(name: str, value: Any) -> Variable:
+ ''' Create or modify a Variable with provided name and value '''
+ name = clean_string(name)
+
+ if is_Variable(value):
+ var = value
+ var(value)
+ else:
+ if name in VARIABLES:
+ var = get_variable(name)
+ var(value)
+ else:
+ var = Variable(name, value)
+
+ VARIABLES[name] = var
+ update_allowed_dict()
+ return var
+
+
+def get_variable(name: str) -> Variable:
+ ''' Return Variable with provided name if exists else None '''
+ assert name in VARIABLES, f"Variable name '{name}' not found in {list_variables()}"
+ return VARIABLES[name]
+
+
+def remove_variable(name: str) -> Variable:
+ var = VARIABLES.pop(name)
+ update_allowed_dict()
+ return var
+
+
+def remove_from_config(variables: List[Tuple[str, Any]]):
+ for name, _ in variables:
+ if name in VARIABLES:
+ remove_variable(name)
+
+
+def update_from_config(variables: List[Tuple[str, Any]]):
+ for var in variables:
+ set_variable(var[0], var[1])
+
+
+def has_variable(value: str) -> bool:
+ if not isinstance(value, str): return False
+ if has_eval(value): value = value[len(EVAL): ]
+
+ pattern = r'[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*'
+ pattern_match = [var.split('.')[0] for var in re.findall(pattern, value)]
+
+ for key in (list(DEVICES) + list(VARIABLES)):
+ if key in pattern_match:
+ return True
+ return False
+
+
+def has_eval(value: Any) -> bool:
+ """ Checks if value is a string starting with '$eval:'"""
+ return True if isinstance(value, str) and value.startswith(EVAL) else False
+
+
+def is_Variable(value: Any):
+ """ Returns True if value of type Variable """
+ return isinstance(value, Variable)
+
+
+def eval_variable(value: Any) -> Any:
+ """ Evaluate the given python string. String can contain variables,
+ devices, numpy arrays and pandas dataframes."""
+ if has_eval(value): value = Variable('temp', value)
+
+ if is_Variable(value): return value()
+ return value
+
+
+def eval_safely(value: Any) -> Any:
+ """ Same as eval_variable but do not evaluate if contains devices or variables """
+ if has_eval(value): value = Variable('temp', value)
+
+ if is_Variable(value): return value.value
+ return value
diff --git a/autolab/core/version_adapter.py b/autolab/core/version_adapter.py
index e0ba4efb..bfd43b80 100644
--- a/autolab/core/version_adapter.py
+++ b/autolab/core/version_adapter.py
@@ -3,6 +3,9 @@
import os
import shutil
+from .paths import PATHS, DRIVER_LEGACY, DRIVER_SOURCES
+from .repository import install_drivers
+
def process_all_changes():
''' Apply all changes '''
@@ -12,31 +15,28 @@ def process_all_changes():
def rename_old_devices_config_file():
''' Rename local_config.ini into devices_config.ini'''
- from .paths import USER_FOLDER
- if os.path.exists(os.path.join(USER_FOLDER, 'local_config.ini')):
- os.rename(os.path.join(USER_FOLDER, 'local_config.ini'),
- os.path.join(USER_FOLDER, 'devices_config.ini'))
+ if (not os.path.exists(os.path.join(PATHS['user_folder'], 'devices_config.ini'))
+ and os.path.exists(os.path.join(PATHS['user_folder'], 'local_config.ini'))):
+ os.rename(os.path.join(PATHS['user_folder'], 'local_config.ini'),
+ os.path.join(PATHS['user_folder'], 'devices_config.ini'))
def move_driver():
""" Move old driver directory to new one """
- from .paths import USER_FOLDER, DRIVERS, DRIVER_LEGACY, DRIVER_SOURCES
- from .repository import install_drivers
-
- if os.path.exists(os.path.join(USER_FOLDER)) and not os.path.exists(DRIVERS):
- os.mkdir(DRIVERS)
- print(f"The new driver directory has been created: {DRIVERS}")
+ if os.path.exists(os.path.join(PATHS['user_folder'])) and not os.path.exists(PATHS['drivers']):
+ os.mkdir(PATHS['drivers'])
+ print(f"The new driver directory has been created: {PATHS['drivers']}")
- # Inside os.path.exists(DRIVERS) condition to avoid moving drivers from current repo everytime autolab is started
+ # Inside os.path.exists(PATHS['drivers']) condition to avoid moving drivers from current repo everytime autolab is started
if os.path.exists(DRIVER_LEGACY['official']):
- shutil.move(DRIVER_LEGACY['official'], DRIVERS)
- os.rename(os.path.join(DRIVERS, os.path.basename(DRIVER_LEGACY['official'])),
+ shutil.move(DRIVER_LEGACY['official'], PATHS['drivers'])
+ os.rename(os.path.join(PATHS['drivers'], os.path.basename(DRIVER_LEGACY['official'])),
DRIVER_SOURCES['official'])
print(f"Old official drivers directory has been moved from: {DRIVER_LEGACY['official']} to: {DRIVER_SOURCES['official']}")
install_drivers() # Ask if want to download official drivers
if os.path.exists(DRIVER_LEGACY["local"]):
- shutil.move(DRIVER_LEGACY['local'], DRIVERS)
- os.rename(os.path.join(DRIVERS, os.path.basename(DRIVER_LEGACY['local'])),
+ shutil.move(DRIVER_LEGACY['local'], PATHS['drivers'])
+ os.rename(os.path.join(PATHS['drivers'], os.path.basename(DRIVER_LEGACY['local'])),
DRIVER_SOURCES['local'])
print(f"Old local drivers directory has been moved from: {DRIVER_LEGACY['local']} to: {DRIVER_SOURCES['local']}")
diff --git a/autolab/core/web.py b/autolab/core/web.py
index 79141603..7c049c8d 100644
--- a/autolab/core/web.py
+++ b/autolab/core/web.py
@@ -10,6 +10,7 @@
import os
import inspect
+from .utilities import open_file
project_url = 'https://github.com/autolab-project/autolab'
drivers_url = 'https://github.com/autolab-project/autolab-drivers'
@@ -26,7 +27,7 @@ def doc(online: bool = "default"):
Can open online or offline documentation by using True or False."""
if online == "default":
- if has_internet(): webbrowser.open(doc_url)
+ if has_internet(False): webbrowser.open(doc_url)
else:
print("No internet connection found. Open local pdf documentation instead")
doc_offline()
@@ -37,11 +38,11 @@ def doc(online: bool = "default"):
def doc_offline():
dirname = os.path.dirname(os.path.abspath(inspect.stack()[0][1]))
filename = os.path.join(dirname, "../autolab.pdf")
- if os.path.exists(filename): os.startfile(filename)
+ if os.path.exists(filename): open_file(filename)
else: print("No local pdf documentation found at {filename}")
-def has_internet() -> bool:
+def has_internet(_print=True) -> bool:
""" https://stackoverflow.com/questions/20913411/test-if-an-internet-connection-is-present-in-python#20913928 """
try:
# see if we can resolve the host name -- tells us if there is
@@ -53,5 +54,5 @@ def has_internet() -> bool:
return True
except Exception: pass # we ignore any errors, returning False
- print("No internet connection found")
+ if _print: print("No internet connection found")
return False
diff --git a/autolab/version.txt b/autolab/version.txt
index 7fb48b5e..cd5ac039 100644
--- a/autolab/version.txt
+++ b/autolab/version.txt
@@ -1 +1 @@
-2.0rc1
+2.0
diff --git a/docs/conf.py b/docs/conf.py
index 3ab86ad5..663a9b70 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -30,8 +30,11 @@
# -- Project information -----------------------------------------------------
project = 'Autolab'
-copyright = '2024, Quentin Chateiller & Bruno Garbin & Jonathan Peltier & Mathieu Jeannin, (C2N-CNRS)'
-author = 'Quentin Chateiller & Bruno Garbin & Jonathan Peltier & Mathieu Jeannin'
+copyright = (
+ "2019-2020 Quentin Chateiller and Bruno Garbin (C2N-CNRS), "
+ "2021-2024 Jonathan Peltier and Mathieu Jeannin (C2N-CNRS)"
+)
+author = 'Q. Chateiller, B. Garbin, J. Peltier and M. Jeannin'
# The full version, including alpha/beta/rc tags
release = version
diff --git a/docs/example_config.conf b/docs/example_config.conf
index 01a445b9..205f636e 100644
--- a/docs/example_config.conf
+++ b/docs/example_config.conf
@@ -1,16 +1,20 @@
{
"autolab": {
- "version": "1.2.1",
- "timestamp": "2023-12-12 22:49:28.540876"
+ "version": "2.0",
+ "timestamp": "2024-07-16 13:58:04.710505"
},
"recipe_1": {
+ "name": "recipe_1",
+ "active": "True",
"parameter": {
- "name": "parameter_buffer",
- "address": "system.parameter_buffer",
- "nbpts": "101",
- "start_value": "0",
- "end_value": "10",
- "log": "0"
+ "parameter_1": {
+ "name": "parameter_buffer",
+ "address": "system.parameter_buffer",
+ "nbpts": "101",
+ "start_value": "0.0",
+ "end_value": "10.0",
+ "log": "0"
+ }
},
"recipe": {
"1_name": "set_amplitude",
diff --git a/docs/example_config2.conf b/docs/example_config2.conf
index d2a55eba..4cdd67e4 100644
--- a/docs/example_config2.conf
+++ b/docs/example_config2.conf
@@ -1,9 +1,11 @@
{
"autolab": {
- "version": "1.2.2",
- "timestamp": "2024-01-24 23:39:44.250166"
+ "version": "2.0",
+ "timestamp": "2024-07-16 13:58:12.991910"
},
"recipe_1": {
+ "name": "recipe_1",
+ "active": "True",
"parameter": {
"parameter_1": {
"name": "parameter",
@@ -21,17 +23,18 @@
"2_name": "array_1D",
"2_steptype": "measure",
"2_address": "mydummy.array_1D"
- },
- "active": "True"
+ }
},
"recipe_2": {
+ "name": "recipe_2",
+ "active": "True",
"parameter": {
"parameter_1": {
"name": "parameter",
"address": "None",
"nbpts": "6",
- "start_value": "0",
- "end_value": "10",
+ "start_value": "0.0",
+ "end_value": "10.0",
"log": "0"
}
},
@@ -42,7 +45,6 @@
"2_name": "phase",
"2_steptype": "measure",
"2_address": "mydummy.phase"
- },
- "active": "True"
+ }
}
}
\ No newline at end of file
diff --git a/docs/example_config3.conf b/docs/example_config3.conf
new file mode 100644
index 00000000..fe25864d
--- /dev/null
+++ b/docs/example_config3.conf
@@ -0,0 +1,49 @@
+{
+ "autolab": {
+ "version": "2.0",
+ "timestamp": "2024-07-16 14:40:23.354948"
+ },
+ "recipe_1": {
+ "name": "recipe",
+ "active": "True",
+ "parameter": {},
+ "recipe": {
+ "1_name": "amplitude",
+ "1_steptype": "set",
+ "1_address": "mydummy.amplitude",
+ "1_value": "0",
+ "2_name": "wait",
+ "2_steptype": "action",
+ "2_address": "system.wait",
+ "2_value": "1.5",
+ "3_name": "amplitude_1",
+ "3_steptype": "set",
+ "3_address": "mydummy.amplitude",
+ "3_value": "10",
+ "4_name": "wait_1",
+ "4_steptype": "action",
+ "4_address": "system.wait",
+ "4_value": "1.5",
+ "5_name": "amplitude_2",
+ "5_steptype": "set",
+ "5_address": "mydummy.amplitude",
+ "5_value": "5",
+ "6_name": "wait_2",
+ "6_steptype": "action",
+ "6_address": "system.wait",
+ "6_value": "2",
+ "7_name": "amplitude_3",
+ "7_steptype": "set",
+ "7_address": "mydummy.amplitude",
+ "7_value": "0",
+ "8_name": "wait_3",
+ "8_steptype": "action",
+ "8_address": "system.wait",
+ "8_value": "2",
+ "9_name": "amplitude_4",
+ "9_steptype": "set",
+ "9_address": "mydummy.amplitude",
+ "9_value": "10"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/gui/about.png b/docs/gui/about.png
new file mode 100644
index 00000000..3020d8d2
Binary files /dev/null and b/docs/gui/about.png differ
diff --git a/docs/gui/add_device.png b/docs/gui/add_device.png
new file mode 100644
index 00000000..7ee64514
Binary files /dev/null and b/docs/gui/add_device.png differ
diff --git a/docs/gui/autocompletion.png b/docs/gui/autocompletion.png
new file mode 100644
index 00000000..55b616ca
Binary files /dev/null and b/docs/gui/autocompletion.png differ
diff --git a/docs/gui/control_center.rst b/docs/gui/control_center.rst
index d86b5119..572c3e93 100644
--- a/docs/gui/control_center.rst
+++ b/docs/gui/control_center.rst
@@ -3,20 +3,23 @@
Control panel
=============
-The Autolab GUI Control Panel provides an easy way to control your instruments.
+The Control Panel provides an easy way to control your instruments.
From it, you can visualize and set the value of its *Variables*, and execute its *Action* through graphical widgets.
-.. image:: control_panel.png
+.. figure:: control_panel.png
+ :figclass: align-center
+
+ Control panel
Devices tree
------------
-By default, the name of each local configuration in represented in a tree widget.
+By default, the name of each local configuration is represented in a tree widget.
Click on one of them to load the associated **Device**.
Then, the corresponding *Element* hierarchy appears.
-Right-click to bring up the close option.
+Right-click to bring up the close options.
-The help of a given **Element** (see :ref:`highlevel`) can be displayed though a tooltip by passing the mouse over it (if provided in the driver files).
+The help of a given **Element** (see :ref:`highlevel`) can be displayed through a tooltip by passing the mouse over it (if provided in the driver files).
Actions
#######
@@ -33,15 +36,16 @@ The value of a *Variable* can be set or read if its type is numerical (integer,
If the *Variable* is readable (read function provided in the driver), a **Read** button is available on its line.
When clicking on this button, the *Variable*'s value is read and displayed in a line edit widget (integer / float values) or in a checkbox (boolean).
-If the *Variable* is writable (write function provided in the driver), its value can be edited and sent to the instrument (return pressed for interger / float values, check box checked or unchecked for boolean values).
-If the *Variable* is also readable, a **Read** operation will be executed automatically after that.
+If the *Variable* is writable (write function provided in the driver), its value can be edited and sent to the instrument (return pressed for integer/float values, check box checked or unchecked for boolean values).
+If the *Variable* is readable, a **Read** operation will be executed automatically after that.
To read and save the value of a *Variable*, right click on its line and select **Read and save as...**.
You will be prompted to select the path of the output file.
The colored displayed at the end of a line corresponds to the state of the displayed value:
- * The orange color means that the currently displayed value is not necessary the current value of the **Variable** in the instrument. The user should click the **Read** button to update the value in the interface.
+ * The orange color means that the currently displayed value is not necessarily the current value of the **Variable** in the instrument. The user should click the **Read** button to update the value in the interface.
+ * The yellow color indicates that the currently displayed value is the last value written to the instrument, but it has not been read back to verify.
* The green color means that the currently displayed value is up to date (except if the user modified its value directly on the instrument. In that case, click the **Read** button to update the value in the interface).
Monitoring
@@ -54,20 +58,88 @@ Please visit the section :ref:`monitoring`.
Slider
------
-A readable and numerical *Variable* can be controled by a slider for convinient setting.
+A readable and numerical *Variable* can be controled by a slider for convenient setting.
To open the slider of this *Variable*, right click on it and select **Create a slider**.
-.. image:: slider.png
+
+.. figure:: slider.png
+ :figclass: align-center
+
+ Slider
+
Scanning
--------
-You can open the scanning interface with the associated button 'Open scanner' in the menu bar of the control panel window.
+You can open the scanning panel with the associated **Scanner** button under the **Panels** sub-menu of the control panel menu bar.
To configure a scan, please visit the section :ref:`scanning`.
Plotting
--------
-You can open the plotting interface with the associated button 'Open plotter' in the menu bar of the control panel window.
-See section :ref:`plotting`.
+You can open the plotting panel with the associated **Plotter** button under the **Panels** sub-menu of the control panel menu bar.
+See section :ref:`plotting` for more details.
+
+
+Other features
+--------------
+
+Logger
+######
+
+A logger can be added to the control center using the variable ``logger = True`` in the section [control_center] of ``autolab_config.ini``.
+It monitors every print functions coming from autolab GUI or drivers to keep track of bugs/errors.
+It is inside a pyqtgraph docker, allowing to detached it from the control panel and place it somewhere visible.
+
+Console
+#######
+
+A Python console can be added to the control center using the variable ``console = True`` in the section [control_center] of ``autolab_config.ini``.
+It allows to inspect autolab or drivers while using the GUI for debugging purposes.
+
+
+Executing Python codes in GUI
+#############################
+
+A function for executing python code directly in the GUI can be used to change a variable based on other device variables or purely mathematical equations.
+
+To use this function both in the control panel and in a scan recipe, use the special ``$eval:`` tag before defining your code in the corresponding edit box.
+This name was chosen in reference to the python `eval` function used to perform the operation and also to be complex enough not to be used by mistake, thereby preventing unexpected results.
+The eval function only has access to all instantiated devices and to the pandas and numpy packages.
+
+.. code-block:: python
+
+ >>> # Usefull to set the value of a parameter in a recipe step
+ >>> $eval:system.parameter_buffer()
+
+ >>> # Useful to define a step according to a measured data
+ >>> $eval:laser.wavelength()
+
+ >>> # Useful to define a step according to an analyzed value
+ >>> $eval:plotter.bandwitdh.x_left()
+ >>> $eval:np.max(mydummy.array_1D())
+
+ >>> # Usefull to define a filename that changes during an analysis
+ >>> $eval:f"data_wavelength={laser.wavelength()}.txt"
+
+ >>> # Usefull to add a dataframe to a device variable (for example to add data using the action `plotter.data.add_data`)
+ >>> $eval:mydummy.array_1D()
+
+It can also be useful in a scan for example to set the central frequency of a spectral analyzer according to the frequency of a signal generator. Here is an example to realize this measurement using ``$eval:``.
+
+.. figure:: recipe_eval_example.png
+ :figclass: align-center
+
+ Recipe using eval example
+
+
+Autocompletion
+###############
+
+To simplify the usage of codes in GUI, an autocompletion feature is accesible by pressing **Tab** after writing ``$eval:`` in any text widget.
+
+.. figure:: autocompletion.png
+ :figclass: align-center
+
+ Autocompletion, console and logger example
diff --git a/docs/gui/control_panel.png b/docs/gui/control_panel.png
index 2cf3fc45..4af722a2 100644
Binary files a/docs/gui/control_panel.png and b/docs/gui/control_panel.png differ
diff --git a/docs/gui/driver_installer.png b/docs/gui/driver_installer.png
new file mode 100644
index 00000000..5a67f0e7
Binary files /dev/null and b/docs/gui/driver_installer.png differ
diff --git a/docs/gui/extra.rst b/docs/gui/extra.rst
index 99e1f449..f6ab8632 100644
--- a/docs/gui/extra.rst
+++ b/docs/gui/extra.rst
@@ -3,54 +3,9 @@
Experimental features
=====================
-Executing Python codes in GUI
-#############################
-
-A function for executing python code directly in the GUI can be used to change a variable based on other device variables or purely mathematical equations.
-
-To use this function both in the control panel and in a scan recipe, use the special ``$eval:`` tag before defining your code in the corresponding edit box.
-This name was chosen in reference to the python function eval used to perform the operation and also to be complex enough not to be used by mistake and produce an unexpected result.
-The eval function only has access to all instantiated devices and to the pandas and numpy packages.
-
-.. code-block:: none
-
- >>> # Usefull to set the value of a parameter to a step of a recipe
- >>> $eval:system.parameter_buffer()
-
- >>> # Useful to define a step according to a measured data
- >>> $eval:laser.wavelength()
-
- >>> # Useful to define a step according to an analyzed value
- >>> $eval:plotter.bandwitdh.x_left()
- >>> $eval:np.max(mydummy.array_1D())
-
- >>> # Usefull to define a filename which changes during an analysis
- >>> $eval:f"data_wavelength={laser.wavelength()}.txt"
-
- >>> # Usefull to add a dataframe to a device variable (for example to add data using the action plotter.data.add_data)
- >>> $eval:mydummy.array_1D()
-
-It can also be useful in a scan for example to set the central frequency of a spectral analyzer according to the frequency of a signal generator. Here is a example to realize this measurement using ``$eval:``.
-
-.. image:: recipe_eval_example.png
-
-
-Logger
-######
-
-A logger can be added to the control center using the variable ``logger = True`` in the section [control_center] of ``autolab_config.ini``.
-It monitor every print functions coming from autolab GUI or drivers to keep track of bugs/errors.
-It is inside a pyqtgraph docker, allowing to detached it from the control panel and place it somewhere visible.
-
-Console
-#######
-
-A Python console can be added to the control center using the variable ``console = True`` in the section [control_center] of ``autolab_config.ini``.
-It allows to inspect autolab or drivers while using the GUI for debugging purposes.
-
Plot from driver
################
-When creating a plot from a driver inside the GUI usualy crashes Python because the created plot isn't connected to the GUI thread.
+When creating a plot from a driver inside the GUI, Python usually crashes because the created plot isn't connected to the GUI thread.
To avoid this issue, a driver can put gui=None as an argument and use the command gui.createWidget to ask the GUI to create the widget and send back the instance.
This solution can be used to create and plot data in a custom widget while using the GUI.
diff --git a/docs/gui/index.rst b/docs/gui/index.rst
index 2a6129f8..51f7fd56 100644
--- a/docs/gui/index.rst
+++ b/docs/gui/index.rst
@@ -3,9 +3,14 @@
Graphical User Interface (GUI)
==============================
-Autolab is provided with a user-friendly graphical interface based on the **Device** interface, that allows the user to interact even more easily with its instruments. It can be used only for local configurations (see :ref:`localconfig`).
+Autolab is provided with a user-friendly graphical interface based on the **Device** interface, that allows the user to interact even more easily with its instruments. It can only be used for local configurations (see :ref:`localconfig`).
-The GUI has four panels : a **Control Panel** that allows to see visually the architecture of a **Device**, and to interact with an instrument through the *Variables* and *Actions*. The **Monitoring Panel** allows the user to monitor a *Variable* in time. The **Scanning Panel** allows the user to configure the scan of a parameter and the execution of a custom recipe for each value of the parameter. The **Plotting Panel** allows the user to plot data.
+The GUI has four panels:
+
+ * a **Control Panel** that allows to see visually the architecture of a **Device**, and to interact with an instrument through the *Variables* and *Actions*.
+ * The **Monitoring Panel** allows the user to monitor a *Variable* over time.
+ * The **Scanning Panel** allows the user to configure the scan of a parameter and execute a custom recipe for each value of the parameter.
+ * The **Plotting Panel** allows the user to plot data.
.. figure:: control_panel.png
:figclass: align-center
@@ -27,16 +32,16 @@ The GUI has four panels : a **Control Panel** that allows to see visually the ar
Plotting panel
-To start the GUI from a Python shell, call the function ``gui`` of the package:
+To start the GUI from a Python shell, call the ``gui`` function of the package:
.. code-block:: python
>>> import autolab
>>> autolab.gui()
-To start the GUI from an OS shell, call:
+To start the GUI from an OS shell, use:
-.. code-block:: none
+.. code-block:: bash
>>> autolab gui
@@ -48,4 +53,5 @@ To start the GUI from an OS shell, call:
monitoring
scanning
plotting
+ miscellaneous
extra
diff --git a/docs/gui/miscellaneous.rst b/docs/gui/miscellaneous.rst
new file mode 100644
index 00000000..e6030fca
--- /dev/null
+++ b/docs/gui/miscellaneous.rst
@@ -0,0 +1,60 @@
+.. _miscellaneous:
+
+Miscellaneous
+=============
+
+Preferences
+-----------
+
+The preferences panel allows to change the main settings saved in the autolab_config.ini and plotter_config.ini files.
+It is accessible in the **Settings** action of the control panel menubar, or in code with ``autolab.preferences()``.
+
+.. figure:: preferences.png
+ :figclass: align-center
+
+ Preference panel
+
+Driver installer
+----------------
+
+The driver installer allows to select individual drivers or all drivers from the main driver github repository.
+It is accessible in the **Settings** action of the control panel menubar, or in code with ``autolab.driver_installer()``.
+
+.. figure:: driver_installer.png
+ :figclass: align-center
+
+ Driver installer
+
+About
+-----
+
+The about window display the versions the autolab version in-used as well as the main necessary packages.
+It is accessible in the **Help** action of the control panel menubar, or in code with ``autolab.about()``.
+
+.. figure:: about.png
+ :figclass: align-center
+
+ About panel
+
+Add device
+----------
+
+The add device window allows to add a device to the device_config.ini file.
+It is accessible by right clicking on the empty area of the control panel tree, or in code with ``autolab.add_device()``.
+
+.. figure:: add_device.png
+ :figclass: align-center
+
+ Add device panel
+
+Variables menu
+--------------
+
+The variables menu allows to add, modify or monitor variables usable in the GUI.
+When a scan recipe is executed, each measured step creates a variable usable by the recipe, allowing to set a value based on the previous measured step without interacting with the instrument twice.
+It is accessible in the **Variables** action of both the control panel and scanner menubar, or in code with ``autolab.variables_menu()``.
+
+.. figure:: variables_menu.png
+ :figclass: align-center
+
+ Variables menu
diff --git a/docs/gui/monitoring.png b/docs/gui/monitoring.png
index 87859f71..e50963f5 100644
Binary files a/docs/gui/monitoring.png and b/docs/gui/monitoring.png differ
diff --git a/docs/gui/monitoring.rst b/docs/gui/monitoring.rst
index a5e6fbec..cda627d6 100644
--- a/docs/gui/monitoring.rst
+++ b/docs/gui/monitoring.rst
@@ -3,9 +3,14 @@
Monitoring
==========
-.. image:: monitoring.png
+The Monitor allows you to monitor a *Variable* in time.
-The Autolab GUI Monitoring allows you to monitor a *Variable* in time. To start a monitoring, right click on the desired *Variable* in the control panel, and click **Start monitoring**. This *Variable* has to be readable (read function provided in the driver) and numerical (integer, float value or 1 to 3D array).
+.. figure:: monitoring.png
+ :figclass: align-center
+
+ Monitoring panel
+
+To start a monitoring, right click on the desired *Variable* in the control panel, and click **Start monitoring**. This *Variable* has to be readable (read function provided in the driver) and numerical (integer, float value or 1 to 3D array).
In the Monitoring window, you can set the **Window length** in seconds. Any points older than this value is removed. You can also set a **Delay** in seconds, which corresponds to a sleep delay between each measure.
@@ -17,6 +22,13 @@ You can display a bar showing the **Min** or **Max** value reached since the beg
The **Mean** option display the mean value of the currently displayed data (not from the beginning).
+The **Pause on scan start** checkbox allows to pause a monitor during a scan to prevent multiple communication with an instrument (prevent bug and speed up execution).
+
+The **start on scan end** checkbox allows to start back the monitoring after a scan.
+
Thanks to the pyqtgraph package, it is possible to monitor images.
-.. image:: monitoring_image.png
+.. figure:: monitoring_image.png
+ :figclass: align-center
+
+ Monitoring images
diff --git a/docs/gui/monitoring_image.png b/docs/gui/monitoring_image.png
index 93d712e8..9c462028 100644
Binary files a/docs/gui/monitoring_image.png and b/docs/gui/monitoring_image.png differ
diff --git a/docs/gui/multiple_recipes.png b/docs/gui/multiple_recipes.png
index 7c24672c..f3f883bc 100644
Binary files a/docs/gui/multiple_recipes.png and b/docs/gui/multiple_recipes.png differ
diff --git a/docs/gui/plotting.png b/docs/gui/plotting.png
index d6827dda..d1c16cec 100644
Binary files a/docs/gui/plotting.png and b/docs/gui/plotting.png differ
diff --git a/docs/gui/plotting.rst b/docs/gui/plotting.rst
index e2498689..fc46ec1d 100644
--- a/docs/gui/plotting.rst
+++ b/docs/gui/plotting.rst
@@ -3,11 +3,17 @@
Plotting
========
-.. image:: plotting.png
+.. figure:: plotting.png
+ :figclass: align-center
-.. caution::
+ Plotting panel
- The plotter still need some work, feed-back are more than welcome (January 2024).
+.. note::
+
+ The plotter still needs some work, feed-back is more than welcome.
+
+The Plotter panel is accessible in the **Plotter** action of the **Panels** sub-menu of the control panel menubar, or by code with ``autolab.plotter()``.
+Data can directly be plotted by passing them as argument ``autolab.plotter(data)``.
Import data
-----------
@@ -35,13 +41,13 @@ The **Plugin** tree can be used to connect any device to the plotter, either by
[plugin]
=
-A plugin do not share the same instance as the original device in the controlcenter, meaning that variables of a device will not affect variables of a plugin and vis-versa.
+A plugin do not share the same instance as the original device in the controlcenter, meaning that variables of a device will not affect variables of a plugin and vice versa.
Because a new instance is created for each plugin, you can add as many plugin from the same device as you want.
-If a device uses the the argument ``gui`` in its ``__init__`` method, it will be able to access the plotter instance to get its data ot to modify the plot itself.
+If a device uses the the argument ``gui`` in its ``__init__`` method, it will be able to access the plotter instance to get its data or to modify the plot itself.
-If a plugin has a method called ``refresh``, the plotter will call it with the argument ``data`` containing the plot data everytime the figure is updated, allowing for each plugin to get the lastest available data and do operations on it.
+If a plugin has a method called ``refresh``, the plotter will call it with the argument ``data`` containing the plot data everytime the figure is updated, allowing for each plugin to get the latest available data and do operations on it.
-The plugin ``plotter`` can be added to the Plotter, allowing to do basic analyzes on the plotted data.
+The plugin ``plotter`` can be added to the Plotter, allowing to do basic analyses on the plotted data.
Among them, getting the min, max values, but also computing the bandwidth around a local extremum.
Note that this plugin can be used as a device to process data in the control panel or directly in a scan recipe.
diff --git a/docs/gui/preferences.png b/docs/gui/preferences.png
new file mode 100644
index 00000000..53e1e853
Binary files /dev/null and b/docs/gui/preferences.png differ
diff --git a/docs/gui/scanning.png b/docs/gui/scanning.png
index dd6d0315..288da5d6 100644
Binary files a/docs/gui/scanning.png and b/docs/gui/scanning.png differ
diff --git a/docs/gui/scanning.rst b/docs/gui/scanning.rst
index f20892b4..a1649374 100644
--- a/docs/gui/scanning.rst
+++ b/docs/gui/scanning.rst
@@ -3,16 +3,19 @@
Scanning
========
-The Autolab GUI Scanning interface allows the user to sweep parameters over a certain range of values, and execute for each of them a custom recipe.
+The Scanner interface allows the user to sweep parameters over a certain range of values, and execute for each of them a custom recipe.
-.. image:: scanning.png
+.. figure:: scanning.png
+ :figclass: align-center
+
+ Scanning panel
Scan configuration
##################
A scan can be composed of several recipes. Click on **Add recipe** at the bottom of the scanner to add an extra recipe.
-A recipe represent a list of steps that are executed for each value of a or multiple parameter.
+A recipe represents a list of steps that are executed for each value of one or multiple parameters.
Parameters
@@ -20,10 +23,10 @@ Parameters
The first step to do is to configure a scan parameter. A parameter is a *Variable* which is writable (write function provided in the driver) and numerical (integer or float value). To set a *Variable* as scan parameter, right click on it on the control panel window, and select **Set as scan parameter**.
-The user can change the name of the parameter with the line edit widget. This name will be used is the data files.
-It it possible to add extra parameters to a recipe by right cliking on the top of a recipe and selecting **Add Parameter**
+The user can change the name of the parameter using the line edit widget. This name will be used in the data files.
+It is possible to add extra parameters to a recipe by right-clicking on the top of a recipe and selecting **Add Parameter**
This feature allows to realize 2D scan or ND-scan.
-A parameter can be removed by right cliking on its frame and selecting **Remove **.
+A parameter can be removed by right-clicking on its frame and selecting **Remove **.
A parameter is optional, a recipe is executed once if no parameter is given.
Parameter range
@@ -31,7 +34,7 @@ Parameter range
The second step is to configure the range of the values that will be applied to the parameter during the scan.
The user can set the start value, the end value, the mean value, the range width, the number of points of the scan or the step between two values.
-The user can also space the points following a log scale by selecting the **Log** option.
+The user can also space the points following a logarithmic scale by selecting the **Log** option.
It is also possible to use a custom array for the parameter using the **Custom** option.
Steps
@@ -47,14 +50,14 @@ Each recipe step must have a unique name. To change the name of a recipe step, r
Recipe steps can be dragged and dropped to modify their relative order inside a recipe, to move them between multiple recipes, or to add them from the control panel. They can also be removed from the recipe using the right click menu **Remove**.
-Right clicking on a recipe gives several options: **Disable**, **Rename**, **Remove**, **Add Parameter**, **Move up** and **Move down**.
+Right-clicking on a recipe gives several options: **Disable**, **Rename**, **Remove**, **Add Parameter**, **Move up** and **Move down**.
-All changes made to the scan configuration are kept in a history allowing changes to be undone or restored using buttons **Undo** and **Redo**. These buttons are accessible using the **Edit** button in the menu bar of the scanner window.
+All changes made to the scan configuration are kept in a history, allowing changes to be undone or restored using the **Undo** and **Redo** buttons. These buttons are accessible using the **Edit** button in the menu bar of the scanner window.
Store the configuration
-----------------------
-Once the configuration of a scan is finished, the user can save it locally in a file for future use, by opening the menu **Configuration** and selecting **Export current configuration**. The user will be prompted for a file path in which the current scan configuration (parameter, parameter range, recipe) will be saved.
+Once the configuration of a scan is finished, the user can save it locally in a file for future use by opening the **Configuration** menu and selecting **Export current configuration**. The user will be prompted for a file path in which the current scan configuration (parameter, parameter range, recipe) will be saved.
To load a previously exported scan configuration, open the menu **Configuration** and select **Import configuration**. The user will be prompted for the path of the configuration file.
Use the **Append** option to append the selected configuration as an extra recipe to the existing scan.
@@ -63,11 +66,10 @@ Alternatively, recently opened configuration files can be accessed via the **Imp
Scan execution
##############
- * **Start** button: start / stop the scan.
+ * **Start** button: start the scan.
* **Pause** button: pause / resume the scan.
+ * **Stop** button: stop the scan.
* **Continuous scan** check box: if checked, start automatically a new scan when the previous one is finished. The state of this check box can be changed at any time.
- * **Clear data** button: delete any previous datapoint recorded.
- * **Save** button: save the data of the last scan. The user will be prompted for a folder path, that will be used to save the data and a screenshot of the figure.
.. note::
@@ -83,14 +85,29 @@ Figure
The user can interact with the figure at any time (during a scan or not).
-After a first loop of a recipe has been processed, the user can select the *Variable* displayed in x and y axis of the figure.
+After the first loop of a recipe has been processed, the user can select the *Variable* displayed in x and y axes of the figure.
-The user can display the previous scan results using the combobox above the scanner figure containing the scan name.
+A data filtering option is available below the figure to select the desired data, allowing for example to plot a slice of a 2D scan.
-If the user has created several recipes in a scan, it is possible to display its results using the combobox above the scanner figure contaning the recipe name.
+A 2D plot option allows to display scan data as a colormap with x, y as axies and z as values, usuful to represent ND-scan.
-It is possible to display arrays and images using the combobox above the scanner figure containing the dataframe name or 'Scan' for the main scan result.
+Scan data can be clear or saved with the buttons bellow the figure.
-A data filtering option is available below the figure to select the desired data, allowing for example to plot a slice of a 2D scan.
+ * **Clear all** button: delete any previous datapoint recorded.
+ * **Save all** button: save all the data of all the executed scans. The user will be prompted for a folder path, that will be used to save the data of all the scans.
+ * **Save** button: save the data of the selected scan. The user will be prompted for a folder path, that will be used to save the data of the scan.
+
+The user can display the previous scan results using the combobox below the scanner figure containing the scan name (scan1, scan2, ...).
+
+If the user has created several recipes in a scan, a combobox below the scanner figure contaning the recipe names (recipe, recipe_1, ...) allows to change the displayed recipe results.
+
+A combobox below the scanner figure containing the dataframe name or 'Scan' for the main scan result allows to display arrays and images.
+
+The button **Scan data** display the scan data in a table.
+
+The button **Send to plotter** send the scan data of the selected recipe to the :ref:`plotting`.
+
+.. figure:: multiple_recipes.png
+ :figclass: align-center
-.. image:: multiple_recipes.png
+ Multiple recipe example
diff --git a/docs/gui/slider.png b/docs/gui/slider.png
index fbc80291..b8e57073 100644
Binary files a/docs/gui/slider.png and b/docs/gui/slider.png differ
diff --git a/docs/gui/variables_menu.png b/docs/gui/variables_menu.png
new file mode 100644
index 00000000..743cdbba
Binary files /dev/null and b/docs/gui/variables_menu.png differ
diff --git a/docs/help_report.rst b/docs/help_report.rst
index 65203edc..a8665fac 100644
--- a/docs/help_report.rst
+++ b/docs/help_report.rst
@@ -1,10 +1,10 @@
-Doc / Reports / Stats
------------------------------------------
+Doc / Reports
+-------------
Documentation
=============
-You can open directly this documentation from Python by calling the function ``doc`` of the package:
+You can directly open this documentation from Python by calling the ``doc`` function of the package:
.. code-block:: python
@@ -19,10 +19,10 @@ You can open directly this documentation from Python by calling the function ``d
Bugs & suggestions reports
==========================
-If you encounter some problems or bugs, or if you have any suggestion to improve this package, or one of its driver, please open an Issue on the GitHub page of this project
+If you encounter any problems or bugs, or if you have any suggestion to improve this package, or one of its drivers, please open an Issue on the GitHub page of this project
https://github.com/autolab-project/autolab/issues/new
-You can also directly call the function ``report`` of the package, which will open this page in your web browser:
+You can also directly call the ``report`` function of the package, which will open this page in your web browser:
.. code-block:: python
diff --git a/docs/high_level.rst b/docs/high_level.rst
index eab3e4a8..9024b573 100644
--- a/docs/high_level.rst
+++ b/docs/high_level.rst
@@ -6,15 +6,15 @@ Devices (High-level interface)
What is a Device?
-----------------
-The high-level interface of Autolab is an abstraction layer of its low-level interface, which allows to communicate easily and safely with laboratory instruments without knowing the structure of its associated **Driver**.
+The high-level interface of Autolab is an abstraction layer of its low-level interface, which allows easy and safe communication with laboratory instruments without knowing the structure of their associated **Driver**.
In this approach, an instrument is fully described with a hierarchy of three particular **Elements**: the **Modules**, the **Variables** and the **Actions**.
* A **Module** is an **Element** that consists in a group of **Variables**, **Actions**, and sub-**Modules**. The top-level **Module** of an instrument is called a **Device**.
-* A **Variable** is an **Element** that refers to a physical quantity, whose the value can be either set and/or read from an instrument (wavelength of an optical source, position of a linear stage, optical power measured with a power meter, spectrum measured with a spectrometer...). Depending on the nature of the physical quantity, it may have a unit.
+* A **Variable** is an **Element** that refers to a physical quantity, whose value can be either set and/or read from an instrument (wavelength of an optical source, position of a linear stage, optical power measured with a power meter, spectrum measured with a spectrometer...). Depending on the nature of the physical quantity, it may have a unit.
-* An **Action** is an **Element** that refers to a particular operation that can be performed by an instrument. (homing of a linear stage, the zeroing of a power meter, the acquisition of a spectrum with a spectrometer...). An **Action** may have a parameter.
+* An **Action** is an **Element** that refers to a particular operation that can be performed by an instrument. (homing of a linear stage, zeroing of a power meter, acquisition of a spectrum with a spectrometer, etc.). An **Action** may have a parameter.
The **Device** of a simple instrument is usually represented by only one **Module**, and a few **Variables** and **Actions** attached to it.
@@ -24,7 +24,7 @@ The **Device** of a simple instrument is usually represented by only one **Modul
|-- Wavelength (Variable)
|-- Output state (Variable)
-Some instruments are a bit more complex, in the sense that they can host several different modules. Their representation in this interface generally consists in one top level **Module** (the frame) and several others sub-**Modules** containing the **Variables** and **Actions** of each associated modules.
+Some instruments are a bit more complex, in the sense that they can host several different modules. Their representation in this interface generally consists of one top level **Module** (the frame) and several others sub-**Modules** containing the **Variables** and **Actions** of each associated module.
.. code-block:: python
@@ -37,12 +37,12 @@ Some instruments are a bit more complex, in the sense that they can host several
|-- Position (Variable)
|-- Homing (Action)
-This hierarchy of **Elements** is implemented for each instrument in its drivers files, and is thus ready to use.
+This hierarchy of **Elements** is implemented for each instrument in its driver files, and is thus ready to use.
Load and close a Device
-----------------------
-The procedure to load a **Device** is almost the same as for the **Driver**, but with the function ``get_device``. You need to provide the nickname of a driver defined in the ``devices_config.ini`` (see :ref:`localconfig`).
+The procedure to load a **Device** is almost the same as for the **Driver**, but with the ``get_device`` function. You need to provide the nickname of a driver defined in the ``devices_config.ini`` (see :ref:`localconfig`).
.. code-block:: python
@@ -50,13 +50,13 @@ The procedure to load a **Device** is almost the same as for the **Driver**, but
.. note::
- You can overwrite temporarily some of the parameters values of a configuration by simply providing them as keywords arguments in the ``get_device`` function:
+ You can temporarily overwrite some of the parameters values of a configuration by simply providing them as keywords arguments in the ``get_device`` function:
.. code-block:: python
>>> laserSource = autolab.get_device('my_tunics', address='GPIB::9::INSTR')
-To close properly the connection to the instrument, simply call its the function ``close`` of the **Device**. This object will not be usable anymore.
+To properly close the connection to the instrument, simply call the ``close`` function of the **Device**. This object will no longer be usable.
.. code-block:: python
@@ -71,7 +71,7 @@ To close the connection to all instruments (devices, not drivers) at once, you c
Navigation and help in a Device
-------------------------------
-The navigation in the hierarchy of **Elements** of a given **Device** is based on relative attributes. For instance, to access the **Variable** ``wavelength`` of the **Module** (**Device**) ``my_tunics``, simply execute the following command:
+Navigation in the hierarchy of **Elements** of a given **Device** is based on relative attributes. For instance, to access the **Variable** ``wavelength`` of the **Module** (**Device**) ``my_tunics``, simply execute the following command:
.. code-block:: python
@@ -84,7 +84,7 @@ In the case of a more complex **Device**, for instance a power meter named ``my_
>>> powerMeter = autolab.get_device('my_power_meter')
>>> powerMeter.channel1.power
-Every **Element** in Autolab is provided with a function ``help`` that can be called to obtain some information about it, but also to know which further **Elements** can be accessed through it, in the case of a **Module**. For a **Variable**, it will display its read and/or write functions (from the driver), its python type, and its unit if provided in the driver. For an **Action**, il will display the associated function in the driver, and its parameter (python type and unit) if it has one. You can also ``print()`` the object to display this help.
+Every **Element** in Autolab is provided with a ``help`` function that can be called to obtain some information about it, but also to know which further **Elements** can be accessed through it, in the case of a **Module**. For a **Variable**, it will display its read and/or write functions (from the driver), its Python type, and its unit if provided in the driver. For an **Action**, il will display the associated function in the driver, and its parameter (Python type and unit) if it has one. You can also ``print()`` the object to display this help.
.. code-block:: python
@@ -113,7 +113,7 @@ If a **Variable** is writable (write function provided in the driver), its curre
>>> lightSource.wavelength(1549)
>>> lightSource.output(True)
-To save locally the value of a readable **Variable**, use its function `save` with the path of the desired output directory (default filename), or file:
+To save the value of a readable **Variable** locally, use its `save` function with the path of the desired output directory (default filename), or file:
.. code-block:: python
@@ -134,7 +134,7 @@ You can execute an **Action** simply by calling its attribute:
Script example
--------------
-With all these commands, you can now create your own Python script. Here is an example of a script that sweep the wavelength of a light source, and measure the power of a power meter:
+With all these commands, you can now create your own Python script. Here is an example of a script that sweeps the wavelength of a light source, and measures the power of a power meter:
.. code-block:: python
diff --git a/docs/index.rst b/docs/index.rst
index 5ebcbd41..33af178b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -8,7 +8,7 @@ Welcome to Autolab's documentation!
**"Forget your instruments, focus on your experiment!"**
-Autolab is a Python package dedicated to control remotely any laboratory instruments and automate scientific experiments in the most user-friendly way. This package provides a set of standardized drivers for about 50 instruments (for now) which are ready to use, and is open to inputs from the community (new drivers or upgrades of existing ones). The configuration required to communicate with a given instrument (connection type, address, ...) can be saved locally to avoid providing it each time. Autolab can also be used either through a Python shell, an OS shell, or a graphical interface.
+Autolab is a Python package dedicated to remotely controlling any laboratory instruments and automating scientific experiments in the most user-friendly way. This package provides a set of standardized drivers for about 50 instruments (for now) which are ready to use, and is open to inputs from the community (new drivers or upgrades of existing ones). The configuration required to communicate with a given instrument (connection type, address, ...) can be saved locally to avoid providing it each time. Autolab can also be used either through a Python shell, an OS shell, or a graphical interface.
.. figure:: scheme.png
:figclass: align-center
@@ -33,7 +33,7 @@ In this package, the interaction with a scientific instrument can be done throug
>>> stage = autolab.get_driver('newport_XPS', connection='SOCKET')
>>> stage.go_home()
- * The :ref:`highlevel`, are an abstraction layer of the low-level interface that provide a simple and straightforward way to communicate with an instrument, through a hierarchy of Modules, Variables and Actions objects.
+ * The :ref:`highlevel`, is an abstraction layer of the low-level interface that provides a simple and straightforward way to communicate with an instrument, through a hierarchy of Modules, Variables and Actions objects.
.. code-block:: python
@@ -52,12 +52,12 @@ In this package, the interaction with a scientific instrument can be done throug
>>> stage = autolab.get_device('my_stage') # Create the Device 'my_stage'
>>> stage.home() # Execute the Action 'home'
- The user can also interact even more easily with this high-level interface through a user-friendly :ref:`gui` which contains three panels: A Control Panel (graphical equivalent of the high-level interface), a Monitor (to monitor the value of a Variable in time) and a Scanner (to scan a Parameter and execute a custom Recipe).
+ The user can also interact even more easily with this high-level interface through a user-friendly :ref:`gui` which contains three panels: a Control Panel (graphical equivalent of the high-level interface), a Monitor (to monitor the value of a Variable in time) and a Scanner (to scan a Parameter and execute a custom Recipe).
.. figure:: gui/scanning.png
:figclass: align-center
-All the Autolab's features are also available through an :ref:`shell_scripts`. interface (Windows and Linux) that can be used to perform for instance a quick single-shot operation without opening explicitely a Python shell.
+All of Autolab's features are also available through an :ref:`shell_scripts` interface (Windows and Linux) that can be used to perform for instance a quick single-shot operation without explicitly opening a Python shell.
.. code-block:: none
@@ -65,9 +65,9 @@ All the Autolab's features are also available through an :ref:`shell_scripts`. i
>>> autolab device -D my_tunics -e wavelength -v 1551
.. note::
- **Useful links**:
+ **Useful Links**:
- * `Slides of our Autolab seminar (March 2020) `_
+ * `Slides from our Autolab seminar (March 2020) `_
* Github project: `github.com/autolab-project/autolab `_
* PyPi project: `pypi.org/project/autolab/ `_
* Online documentation: `autolab.readthedocs.io/ `_
@@ -85,6 +85,7 @@ Table of contents:
gui/index
shell/index
help_report
+ release_notes
about
Last edit: |today| for the version |release|
diff --git a/docs/installation.rst b/docs/installation.rst
index acea8654..50e8ba5f 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -4,11 +4,11 @@ Installation
Python
------
-This package is working on Python version 3.6+.
+This package works on Python version 3.6+.
-* On Windows, we recommend to install Python through the distribution Anaconda: https://www.anaconda.com/
-* On older versions of Windows (before Windows 7), we recommend to install Python manually: https://www.python.org/
-* On Linux, we recommend to install Python through the apt-get command.
+* On Windows, we recommend installing Python through the distribution Anaconda: https://www.anaconda.com/
+* On older versions of Windows (before Windows 7), we recommend installing Python manually: https://www.python.org/
+* On Linux, we recommend installing Python through the `apt-get` command.
Additional required packages (installed automatically with Autolab):
@@ -25,14 +25,14 @@ Additional required packages (installed automatically with Autolab):
Autolab package
---------------
-This project is hosted in the global python repository PyPi at the following address : https://pypi.org/project/autolab/.
-To install the Autolab python package on your computer, we then advice you to use the Python package manager ``pip`` in a Python environnement:
+This project is hosted in the global Python repository PyPi at the following address: https://pypi.org/project/autolab/.
+To install the Autolab python package on your computer, we then advise you to use the Python package manager ``pip`` in a Python environnement:
.. code-block:: none
pip install autolab
-If the package is already installed, you can check the current version installed and upgrade it to the last official version with the following commands:
+If the package is already installed, you can check the current version installed and upgrade it to the latest official version with the following commands:
.. code-block:: none
@@ -49,7 +49,7 @@ Import the Autolab package in a Python shell to check that the installation is c
Packages for the GUI
--------------------
-The GUI requires several packages to work. But depending if you are using Anaconda or not, the installation is different:
+The GUI requires several packages to work, but depending on whether you are using Anaconda or not, the installation is different:
With Anaconda:
@@ -67,7 +67,7 @@ Without:
pip install qtpy
pip install pyqt5
-Note that thanks to qtpy, you can install a different qt backend instead of pyqt5 among pyqt6, pyside2 and pyside6
+Note that thanks to qtpy, you can install a different Qt backend instead of pyqt5, such as pyqt6, pyside2, or pyside6
Development version
-------------------
diff --git a/docs/local_config.rst b/docs/local_config.rst
index 963b0398..174783f1 100644
--- a/docs/local_config.rst
+++ b/docs/local_config.rst
@@ -3,16 +3,16 @@
Local configuration
===================
-To avoid having to provide each time the full configuration of an instrument (connection type, address, port, slots, ...) to load a **Device**, Autolab proposes to store it locally for further use.
+To avoid having to provide the full configuration of an instrument (connection type, address, port, slots, etc.) each time to load a **Device**, Autolab proposes storing it locally for further use.
More precisely, this configuration is stored in a local configuration file named ``devices_config.ini``, which is located in the local directory of Autolab. Both this directory and this file are created automatically in your home directory the first time you use the package (the following messages will be displayed, indicating their exact paths).
-.. code-block:: python
+.. code-block:: none
The local directory of AUTOLAB has been created: C:\Users\\autolab.
It contains the configuration files devices_config.ini, autolab_config.ini and plotter.ini.
It also contains the 'driver' directory with 'official' and 'local' sub-directories.
-
+
.. warning ::
Do not move or rename the local directory nor the configuration file.
@@ -23,13 +23,13 @@ A device configuration is composed of several parameters:
* The name of the associated Autolab **driver**.
* All the connection parameters (connection, address, port, slots, ...)
-To see the list of the available devices configurations, call the function ``list_devices``.
+To see the list of the available device configurations, call the ``list_devices`` function.
.. code-block:: python
>>> autolab.list_devices()
-To know what parameters have to be provided for a particular **Device**, use the function `config_help` with the name of corresponding driver.
+To know what parameters have to be provided for a particular **Device**, use the `config_help` function with the name of the corresponding driver.
.. code-block:: python
@@ -52,7 +52,7 @@ This file is structured in blocks, each of them containing the configuration of
slot1 =
slot1_name =
-To see a concrete example of the block you have to append in the configuration file for a given driver, call the function ``config_help`` with the name of the driver. You can then directly copy and paste this exemple into the configuration file, and customize the value of the parameters to suit those of your instrument. Here is an example for the Yenista Tunics light source:
+To see a concrete example of the block you have to append in the configuration file for a given driver, call the ``config_help`` function with the name of the driver. You can then directly copy and paste this exemple into the configuration file, and customize the value of the parameters to suit those of your instrument. Here is an example for the Yenista Tunics light source:
.. code-block:: none
diff --git a/docs/low_level/create_driver.rst b/docs/low_level/create_driver.rst
index 275ebe31..c78e510d 100644
--- a/docs/low_level/create_driver.rst
+++ b/docs/low_level/create_driver.rst
@@ -3,7 +3,7 @@
Write your own Driver
=====================
-The goal of this tutorial is to present the general structure of the drivers of this package, in order for you to create simply your own drivers, and make them available to the community within this collaborative project. We notably provide a fairly understandable driver structure that can handle the highest degree of instruments complexity (including: single and multi-channels function generators, oscilloscopes, Electrical/Optical frames with associated interchangeable submodules, etc.). This provides reliable ways to add other types of connection to your driver (e.g. GPIB to Ethenet) or other functions (e.g. get_amplitude, set_frequency, etc.).
+The goal of this tutorial is to present the general structure of the drivers of this package, in order for you to simply create your own drivers and make them available to the community within this collaborative project. We notably provide a fairly understandable driver structure that can handle the highest degree of instruments complexity (including: single and multi-channels function generators, oscilloscopes, olectrical/optical frames with associated interchangeable submodules, etc.). This provides reliable ways to add other types of connection to your driver (e.g. GPIB to Ethenet) or other functions (e.g. get_amplitude, set_frequency, etc.).
.. note::
@@ -22,7 +22,7 @@ Getting started: create a new driver
Each driver name should be unique: do not define new drivers (in your local folders) with a name that already exists in the main package.
-In the local_drivers directory, as in the main package, each instrument has/should have its own directory organized and named as follow. The name of this folder take the form *\_\*. The driver associated to this instrument is a python script taking the same name as the folder: *\_\.py*. A second python script, allowing the parser to work properly, should be named *\_\_utilities.py* (`find a minimal template here `_). Additional python scripts may be present in this folder (devices's modules, etc.). Please see the existing drivers of the autolab package for extensive examples.
+In the local_drivers directory, as in the main package, each instrument has/should have its own directory organized and named as follow. The name of this folder takes the form *\_\*. The driver associated to this instrument is a python script taking the same name as the folder: *\_\.py*. A second python script, allowing the parser to work properly, should be named *\_\_utilities.py* (`find a minimal template here `_). Additional python scripts may be present in this folder (devices's modules, etc.). Please see the existing drivers of the autolab package for extensive examples.
**For addition to the main package**: Once you tested your driver and it is ready to be used by others, you can send the appropriate directory to the contacts (:ref:`about`).
@@ -38,7 +38,7 @@ Driver structure (*\_\.py* file)
The Driver is organized in several `python class `_ with a structure as follow. The numbers represent the way sections appear from the top to the bottom of an actual driver file. We chose to present the sections in a different way:
-1 - import modules (optionnal)
+1 - import modules (optional)
###############################
To import possible additional modules, e.g.:
@@ -53,10 +53,10 @@ The Driver is organized in several `python class `_ :
+ Examples of VISA addresses can be find online `here `_ :
.. code-block:: python
@@ -172,11 +172,11 @@ The Driver is organized in several `python class _\.py* but the class **Driver_CONNECTION** (including the class Driver and any optionnal class **Module_MODEL**), in order for many features of the package to work properly. It simply consists in a list of predefined elements that will indicate to the package the structure of the driver and predefined variable and actions.
-There are three possible elements in the function ``get_driver_model``: *Module*, *Variable* and *Action*.
+The ``get_driver_model`` function should be present in each of the classes of the *\_\.py* but the class **Driver_CONNECTION** (including the class Driver and any optional class **Module_MODEL**), in order for many features of the package to work properly. It simply consists in a list of predefined elements that will indicate to the package the structure of the driver and predefined variable and actions.
+There are three possible elements in the ``get_driver_model`` function: *Module*, *Variable* and *Action*.
Shared by the three elements (*Module*, *Variable*, *Action*):
- 'name': nickname for your element (argument type: string)
- 'element': element type, exclusively in: 'module', 'variable', 'action' (argument type: string)
- - 'help': quick help, optionnal (argument type: string)
+ - 'help': quick help, optional (argument type: string)
*Module*:
- - 'object' : attribute of the class (argument type: Instance)
+ - 'object': attribute of the class (argument type: Instance)
*Variable*:
- 'read': class attribute (argument type: function)
- 'write': class attribute (argument type: function)
- 'type': python type, exclusively in: int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame
- - 'unit': unit of the variable, optionnal (argument type: string)
- - 'read_init': bool to tell :ref:`control_panel` to read variable on instantiation, optionnal
+ - 'unit': unit of the variable, optional (argument type: string)
+ - 'read_init': bool to tell :ref:`control_panel` to read variable on instantiation, optional
.. caution::
Either 'read' or 'write' key, or both of them, must be provided.
@@ -450,7 +450,7 @@ Shared by the three elements (*Module*, *Variable*, *Action*):
*Action*:
- 'do': class attribute (argument type: function)
- 'param_type': python type, exclusively in: int, float, bool, str, bytes, tuple, np.ndarray, pd.DataFrame, optional
- - 'param_unit': unit of the variable, optionnal (argument type: string. Use special param_unit 'open-file' to open a open file dialog, 'save-file' to open a save file dialog and 'user-input' to open an input dialog)
+ - 'param_unit': unit of the variable, optional (argument type: string. Use special param_unit 'open-file' to open a open file dialog, 'save-file' to open a save file dialog and 'user-input' to open an input dialog)
diff --git a/docs/low_level/index.rst b/docs/low_level/index.rst
index 6ba44550..dd980b03 100644
--- a/docs/low_level/index.rst
+++ b/docs/low_level/index.rst
@@ -6,11 +6,11 @@ Drivers (Low-level interface)
In Autolab, a **Driver** refers to a Python class dedicated to communicate with one particular instrument.
This class contains functions that perform particular operations, and may also contain subclasses in case some modules or channels are present in the instrument.
Autolab comes with a set of about 50 different **Drivers**, which are ready to use.
-As of version 1.2, drivers are now in a seperate GitHub repository located at `github.com/autolab-project/autolab-drivers `_.
-When installing autolab, the user is asked if they wants to install all drivers from this repository.
+As of version 1.2, drivers are now in a separate GitHub repository located at `github.com/autolab-project/autolab-drivers `_.
+When installing autolab, the user is asked if they want to install all drivers from this repository.
The first part of this section explains how to configure and open a **Driver**, and how to use it to communicate with your instrument.
-The, we present the guidelines to follow for the creation of new driver files, to contribute to the Autolab Python package.
+Then, we present the guidelines to follow for the creation of new driver files, to contribute to the Autolab Python package.
Table of contents:
diff --git a/docs/low_level/open_and_use.rst b/docs/low_level/open_and_use.rst
index b50c0d6b..085df557 100644
--- a/docs/low_level/open_and_use.rst
+++ b/docs/low_level/open_and_use.rst
@@ -7,7 +7,7 @@ The low-level interface provides a raw access to the drivers implemented in Auto
.. attention::
- The Autolab drivers may contains internal functions, that are not dedicated to be called by the user, and some functions requires particular types of inputs. **The authors declines any responsibility for the consequences of an incorrect use of the drivers**. To avoid any problems, make sure you have a real understanding of what you are doing, or prefer the use of the :ref:`highlevel`.
+ Autolab drivers may contain internal functions, that are not dedicated to be called by the user, and some functions requires particular types of inputs. **The authors decline any responsibility for the consequences of an incorrect use of the drivers**. To avoid any problems, make sure you have a real understanding of what you are doing, or prefer the use of the :ref:`highlevel`.
To see the list of available drivers in Autolab, call the ``list_drivers`` function.
@@ -25,7 +25,7 @@ Load and close a Driver
-The instantiation of a *Driver* object is done through the function ``get_driver`` of Autolab, and requires a particular configuration:
+The instantiation of a *Driver* object is done using the ``get_driver`` function of Autolab, and requires a particular configuration:
* The name of the driver: one of the name appearing in the ``list_drivers`` function (ex: 'yenista_TUNICS').
* The connection parameters as keywords arguments: the connection type to use to communicate with the instrument ('VISA', 'TELNET', ...), the address, the port, the slots, ...
@@ -34,13 +34,13 @@ The instantiation of a *Driver* object is done through the function ``get_driver
>>> laserSource = autolab.get_driver('yenista_TUNICS', 'VISA', address='GPIB0::12::INSTR')
-To know what is the required configuration to interact with a given instrument, call the function ``config_help`` with the name of the driver.
+To know what is the required configuration to interact with a given instrument, call the ``config_help`` function with the name of the driver.
.. code-block:: python
>>> autolab.config_help('yenista_TUNICS')
-To close properly the connection to the instrument, simply call its the function ``close`` of the **Driver**.
+To close properly the connection to the instrument, simply call the ``close`` function of the **Driver**.
.. code-block:: python
@@ -57,7 +57,7 @@ You are now ready to use the functions implemented in the **Driver**:
>>> laserSource.get_wavelength()
1550
-You can get the list of the available functions by calling the function ``autolab.explore_driver`` with the instance of your **Driver**. Once again, note that some of these functions are note supposed to be used directly, some of them may be internal functions.
+You can get the list of the available functions by calling the ``autolab.explore_driver`` function with the instance of your **Driver**. Once again, note that some of these functions are not supposed to be used directly, some of them may be internal functions.
>>> autolab.explore_driver(laserSource)
diff --git a/docs/release_notes.rst b/docs/release_notes.rst
new file mode 100644
index 00000000..eb046be1
--- /dev/null
+++ b/docs/release_notes.rst
@@ -0,0 +1,114 @@
+Release notes
+=============
+
+2.0
+###
+
+Autolab 2.0 released in 2024 is the first major release since 2020.
+
+General Features
+----------------
+
+- Configuration Enhancements:
+
+ - Enhanced configuration options for driver management in autolab_config.ini, including extra paths and URLs for driver downloads.
+ - Added install_driver() to download drivers.
+ - Improved handling of temporary folders and data saving options.
+
+- Driver Management:
+
+ - Moved drivers to a dedicated GitHub repository: https://github.com/autolab-project/autolab-drivers.
+ - Drivers are now located in the local "/autolab/drivers/official" folder instead of the main package.
+ - Added the ability to download drivers from GitHub using the GUI, allowing selective driver installation.
+
+- Documentation:
+
+ - Added documentation for new features and changes.
+
+GUI Enhancements
+----------------
+
+- General Improvements:
+
+ - Switched from matplotlib to pyqtgraph for better performance and compatibility.
+ - Enhanced plotting capabilities in the monitor and scanner, including support for 1D and 2D arrays and images.
+ - Added $eval: special tag to execute Python code in the GUI to perform custom operations.
+ - Added autocompletion for variables using tabulation.
+ - Added sliders to variables to tune values.
+
+- Control Panel:
+
+ - Added the ability to display and set arrays and dataframes in the control panel.
+ - Added possibility to use variable with type bytes and action that have parameters with type bool, bytes, tuple, array or dataframe.
+ - Added yellow indicator for written but not read elements.
+ - Introduced a checkbox option to optionally display arrays and dataframes in the control panel.
+ - Added sub-menus for selecting recipes and parameters.
+ - Improved device connection management with options to modify or cancel connections.
+ - Added right-click options for modifying device connections.
+
+- Scanner:
+
+ - Implemented multi-parameter and multi-recipe scanning, allowing for more complex scan configurations.
+ - Enhanced recipe management with right-click options for enabling/disabling, renaming, and deleting.
+ - Enabled plotting of scan data as an image, useful for 2D scans.
+ - Added support for custom arrays and parameters in scans.
+ - Enabled use of a default scan parameter not linked to any device.
+ - Added data display filtering option.
+ - Added scan config history with the last 10 configurations.
+ - Added variables to be used in the scan, allowing on-the-fly analysis inside a recipe.
+ - Changed the scan configuration file format from ConfigParser to json to handle new scan features.
+ - Add shortcut for copy paste, undo redo, delete in scanner for recipe steps.
+
+- Plotter:
+
+ - Implementation of a plotter to open previous scan data, connect to instrument variables and perform data analysis.
+
+- Usability Improvements:
+
+ - Enabled drag-and-drop functionality in the GUI.
+ - Added icons and various UI tweaks for better usability.
+ - Enabled opening configuration files from the GUI.
+
+- Standalone GUI Utilities:
+
+ - Added autolab.about() for autolab information.
+ - Added autolab.slider(variable) to change a variable value.
+ - Added autolab.variables_menu() to control variables, monitor or use slider.
+ - Added autolab.add_device() for adding devices to the config file.
+ - Added autolab.monitor(variable) for monitoring variables.
+ - Added autolab.plotter() to open the plotter directly.
+
+Device and Variable Management
+------------------------------
+
+- Variable and Parameter Handling:
+
+ - Added new action units ('user-input', 'open-file', 'save-file') to open dialog boxes.
+ - Added 'read_init' argument to variable allowing to read a value on device instantiation in the control panel.
+ - Added new type 'tuple' to create a combobox in the control panel.
+
+Miscellaneous Improvements
+--------------------------
+
+- Code Quality and Compatibility:
+
+ - Numerous bug fixes to ensure stability and usability across different modules and functionalities.
+ - Compatibility from Python 3.6 up to 3.12.
+ - Switched from PyQt5 to qtpy to enable extensive compatibility (Qt5, Qt6, PySide2, PySide6).
+ - Extensive code cleanup, PEP8 compliance, and added type hints.
+
+- Logger and Console Outputs:
+
+ - Added an optional logger in the control center to display console outputs.
+ - Added an optional console in the control center for debug/dev purposes.
+
+- Miscellaneous:
+
+ - Added an "About" window showing versions, authors, license, and project URLs.
+ - Implemented various fixes for thread handling and error prevention.
+ - Add dark theme option for GUI.
+
+1.1.12
+######
+
+Last version developed by the original authors.
diff --git a/docs/scheme.png b/docs/scheme.png
index 1dcaeecb..0fbd61e3 100644
Binary files a/docs/scheme.png and b/docs/scheme.png differ
diff --git a/docs/shell/connection.rst b/docs/shell/connection.rst
index bb47d923..f3adae58 100644
--- a/docs/shell/connection.rst
+++ b/docs/shell/connection.rst
@@ -15,7 +15,7 @@ Three helps are configured (device or driver may be used equally in the lines be
>>> autolab driver -h
- It including arguments and options formatting, definition of the available options and associated help and informations to retrieve the list of available drivers and local configurations (command: autolab infos).
+ It includes arguments and options formatting, definition of the available options, associated help, and information to retrieve the list of available drivers and local configurations (command: autolab infos).
2) Basic help about the particular name driver/device you provided:
@@ -25,7 +25,7 @@ Three helps are configured (device or driver may be used equally in the lines be
It includes the category of the driver/device (e.g. Function generator, Oscilloscope, etc.), a list of the implemented connections (-C option), personnalized usage example (automatically generated from the driver.py file), and examples to use and set up a local configuration using command lines (see :ref:`localconfig` for more informations about local configurations).
- 3) Full help message **about the driver/device**:
+ 3) Full help message **for the driver/device**:
.. code-block:: none
@@ -40,7 +40,7 @@ Three helps are configured (device or driver may be used equally in the lines be
It includes the hierarchy of the device and all the defined *Modules*, *Variables* and *Actions* (see :ref:`get_driver_model` and :ref:`os_device` for more informations on the definition and usage respectively).
- Note that this help requires the instantiation of your instrument to be done, in other words it requires valid arguments for options -D, -C and -A (that you can get for previous helps) and a working physical link.
+ Note that this help requires the instantiation of your instrument to be done, in other words it requires valid arguments for options -D, -C and -A (that you can get from previous helps) and a working physical link.
.. _name_shell_connection:
@@ -56,7 +56,7 @@ A typical command line structure is:
>>> autolab driver -D -C -A (optional)
>>> autolab device -D (optional)
-**To set up the connection** for the first time, we recommand to follow the different help states (see :ref:`name_shell_help`), that usually guide you through filling the arguments corresponding to the above options. To use one of Autolab's driver to drive an instrument you need to provide its name. This is done with the option -D. -D option accepts a driver_name for a driver (e.g. agilent_33220A, etc) and a config_name for a device (nickname as defined in your device_config.ini, e.g. my_agilent). A full list of the available driver names and config names may be found using the command ``autolab infos``. Due to Autolab's drivers structure you also need to provide a -C option for the connection type (corresponding to a class to use for the communication, see :ref:`create_driver` for more informations) when instantiating your device. The available connection types (arguments for -C option) are driver dependent (you need to provide a valid -D option) and may be access with a second stage help (see :ref:`name_shell_help`).
+**To set up the connection** for the first time, we recommend following the different help states (see :ref:`name_shell_help`), that usually guide you through filling the arguments corresponding to the above options. To use one of Autolab's driver to drive an instrument you need to provide its name. This is done with the option -D. -D option accepts a driver_name for a driver (e.g. agilent_33220A, etc) and a config_name for a device (nickname as defined in your device_config.ini, e.g. my_agilent). A full list of the available driver names and config names may be found using the command ``autolab infos``. Due to Autolab's drivers structure you also need to provide a -C option for the connection type (corresponding to a class to use for the communication, see :ref:`create_driver` for more informations) when instantiating your device. The available connection types (arguments for -C option) are driver dependent (you need to provide a valid -D option) and may be accessed with a second stage help (see :ref:`name_shell_help`).
Lately you will need to provide additional options/arguments to set up the communication. One of the most common is the address for which we cannot help much. At this stage you need to make sure of the instrument address/set the address (on the physical instrument) and format it the way that the connection type is expecting it (e.g. for an ethernet connection with address 192.168.0.1 using VISA connection type: ``TCPIP::192.168.0.1::INSTR``). You will find in the second stage help automatically generated example of a minimal command line (as defined in the driver) that should be able to instantiate your instrument (providing you modify arguments to fit your conditions).
**Other arguments** may be necessary for the driver to work properly. In particular, additional connection argument may be passed through the option -O, such as the port number (for SOCKET connection type), the gpib board index (for GPIB connection) or the path to the dll library (for DLL connection type).
diff --git a/docs/shell/device.rst b/docs/shell/device.rst
index f947c0ca..d8d1b0f9 100644
--- a/docs/shell/device.rst
+++ b/docs/shell/device.rst
@@ -38,7 +38,7 @@ The available operations are listed below:
>>> autolab device -D myLinearStage -e goHome
- * **To display the help** of any **Element**, provide its address with the option ``-h`` or ``--help`` :
+ * **To display the help** of any **Element**, provide its address with the option ``-h`` or ``--help``:
.. code-block:: none
diff --git a/docs/shell/driver.rst b/docs/shell/driver.rst
index cd45bd55..19e5d45a 100644
--- a/docs/shell/driver.rst
+++ b/docs/shell/driver.rst
@@ -4,7 +4,7 @@ Command driver
==============
-See :ref:`name_shell_connection` for more informations about the connection. Once your driver is instantiated you will be able to perform **pre-configured operations** (see :ref:`name_driver_utilities.py` for how to configure operations) as well as **raw operations** (-m option). We will discuss both of them here as well as a quick (bash) **scripting example**.
+See :ref:`name_shell_connection` for more information about the connection. Once your driver is instantiated you will be able to perform **pre-configured operations** (see :ref:`name_driver_utilities.py` for how to configure operations) as well as **raw operations** (-m option). We will discuss both of them here as well as a quick (bash) **scripting example**.
In the rest of this sections we will assume that you have a driver (not device) named instrument that needs a connection named CONN.
@@ -17,7 +17,7 @@ You may access an extensive driver help, that will particularly **list the pre-d
>>> autolab driver -D instrument -C CONN -h
-It includes the list of the implemented connections, the list of the available additional modules (classes **Channel**, **Trace**, **Module_MODEL**, etc.; see :ref:`create_driver`), the list of all the methods that are instantiated with the driver (for direct use with the command: autolab driver; see :ref:`os_driver`), and an extensive help for the usage of the pre-defined options. For instance if an option -a has been defined in the file driver_utilities.py (see :ref:`name_driver_utilities.py`), one may use it to perform the associated action, say to modify the amplitude, this way:
+It includes the list of the implemented connections, the list of the available additional modules (classes **Channel**, **Trace**, **Module_MODEL**, etc.; see :ref:`create_driver`), the list of all the methods that are instantiated with the driver (for direct use with the command: autolab driver; see :ref:`os_driver`), and an extensive help for the usage of the pre-defined options. For instance, if an option -a has been defined in the driver_utilities.py file (see :ref:`name_driver_utilities.py`), one may use it to perform the associated action, such as to modify the amplitude, this way:
.. code-block:: none
@@ -35,7 +35,7 @@ In addition, if the instrument has several channels, an channel option is most l
No space must be present within an argument or option (e.g. do not write ``- c`` or ``-c 4, 6``).
-Furthermore, several operations may be perform in a single and compact script line. One can modify the amplitude of channel 4 and 6 to 2 Volts and the frequencies (of the same channel) to 50 Hz using:
+Furthermore, several operations may be performed in a single and compact script line. One can modify the amplitude of channel 4 and 6 to 2 Volts and the frequencies (of the same channel) to 50 Hz using:
.. code-block:: none
@@ -54,7 +54,7 @@ Furthermore, several operations may be perform in a single and compact script li
Raw operations (-m option)
##########################
-Independently of the user definition of options in the file driver_utilities.py, you may access any methods that are instantiated with the driver using the -m option.
+Regardless of the user's definition of options in the driver_utilities.py file, you may access any methods that are instantiated with the driver using the -m option.
.. important::
@@ -74,7 +74,7 @@ This allow you to simply copy and paste the method you want to use from the list
>>> autolab driver -D instrument -C CONN -m get_amplitude()
>>> autolab driver -D instrument -C CONN -m set_amplitude(value)
-One may also call several methods separated with a space after -m option:
+One may also call several methods separated by a space after the -m option:
.. code-block:: none
@@ -89,7 +89,7 @@ Script example
##############
-One may stack in a single file several script line in order to perform custom measurement (modify several control parameters, etc.). This is a bash counterpart to the python scripting example provided there :ref:`name_pythonscript_example`.
+One may stack several script lines in a single file in order to perform custom measurements (modify several control parameters, etc.). This is a bash counterpart to the Python scripting example provided there :ref:`name_pythonscript_example`.
.. code-block:: none
diff --git a/docs/shell/index.rst b/docs/shell/index.rst
index 84f40385..a2ee0454 100644
--- a/docs/shell/index.rst
+++ b/docs/shell/index.rst
@@ -3,7 +3,7 @@
OS shell
========
-Most of the Autolab functions can also be used directly from a **Windows** or **Linux** terminal without opening explicitely a Python shell.
+Most of the Autolab functions can also be used directly from a **Windows** or **Linux** terminal without opening explicitly a Python shell.
Just execute the command ``autolab`` or ``autolab -h`` or ``autolab --help`` in your terminal to see the available subcommands.
@@ -13,15 +13,15 @@ Just execute the command ``autolab`` or ``autolab -h`` or ``autolab --help`` in
C:\Users\qchat> autolab -h
Hostname:/home/User$ autolab --help
-The subcommands are :
+The subcommands are:
-* ``autolab gui`` : a shortcut of the python function autolab.gui() to start the graphical interface of Autolab.
-* ``autolab install_drivers`` : a shortcut of the python function autolab.install_drivers() to install drivers from GitHub
-* ``autolab driver`` : a shortcut of the python interface Driver (see :ref:`os_driver`)
-* ``autolab device`` : a shortcut of the python interface Device (see :ref:`os_device`)
-* ``autolab doc`` : a shortcut of the python function autolab.doc() to open the present online documentation.
-* ``autolab report`` : a shortcut of the python function autolab.report() to open the present online documentation.
-* ``autolab infos`` : a shortcut of the python function autolab.infos() to list the drivers and the local configurations available on your system.
+* ``autolab gui``: a shortcut of the python function autolab.gui() to start the graphical interface of Autolab.
+* ``autolab install_drivers``: a shortcut of the python function autolab.install_drivers() to install drivers from GitHub
+* ``autolab driver``: a shortcut of the python interface Driver (see :ref:`os_driver`)
+* ``autolab device``: a shortcut of the python interface Device (see :ref:`os_device`)
+* ``autolab doc``: a shortcut of the python function autolab.doc() to open the present online documentation.
+* ``autolab report``: a shortcut of the python function autolab.report() to open the present online documentation.
+* ``autolab infos``: a shortcut of the python function autolab.infos() to list the drivers and the local configurations available on your system.
Table of contents:
diff --git a/setup.py b/setup.py
index aa9f0016..e7387b59 100644
--- a/setup.py
+++ b/setup.py
@@ -18,8 +18,10 @@
setup(
name = 'autolab',
version = version, # Ideally should be same as your GitHub release tag varsion
- author = 'Quentin Chateiller & Bruno Garbin & Jonathan Peltier & Mathieu Jeannin',
+ author = 'Quentin Chateiller & Bruno Garbin',
author_email = 'autolab-project@googlegroups.com',
+ maintainer = 'Jonathan Peltier & Mathieu Jeannin',
+ maintainer_email = 'autolab-project@googlegroups.com',
license = "GPL-3.0 license",
description = 'Python package for scientific experiments interfacing and automation',
long_description = long_description,