# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file."""Main window===========The :mod:`cdl.core.gui.main` module provides the main window of theDataLab (CDL) project... autoclass:: CDLMainWindow"""# pylint: disable=invalid-name # Allows short reference names like x, y, ...from__future__importannotationsimportabcimportbase64importfunctoolsimportosimportos.pathasospimportsysimporttimeimportwebbrowserfromtypingimportTYPE_CHECKINGimportguidata.datasetasgdsimportnumpyasnpimportscipy.ndimageasspiimportscipy.signalasspsfromguidataimportqthelpersasguidata_qthfromguidata.configtoolsimportget_iconfromguidata.qthelpersimportadd_actions,create_actionfromguidata.widgets.consoleimportDockableConsolefromplotpyimportconfigasplotpy_configfromplotpy.builderimportmakefromplotpy.constantsimportPlotTypefromqtpyimportQtCoreasQCfromqtpyimportQtGuiasQGfromqtpyimportQtWidgetsasQWfromqtpy.compatimportgetopenfilenames,getsavefilenameimportcdlfromcdlimport__docurl__,__homeurl__,__supporturl__,envfromcdl.configimport(APP_DESC,APP_NAME,DATAPATH,DEBUG,IS_FROZEN,TEST_SEGFAULT_ERROR,Conf,_,)fromcdl.core.baseproxyimportAbstractCDLControlfromcdl.core.gui.actionhandlerimportActionCategoryfromcdl.core.gui.docksimportDockablePlotWidgetfromcdl.core.gui.h5ioimportH5InputOutputfromcdl.core.gui.panelimportbase,image,macro,signalfromcdl.core.gui.settingsimportedit_settingsfromcdl.core.model.imageimportImageObj,create_imagefromcdl.core.model.signalimportSignalObj,create_signalfromcdl.core.remoteimportRemoteServerfromcdl.envimportexecenvfromcdl.pluginsimportPluginRegistry,discover_pluginsfromcdl.utilsimportdephashfromcdl.utilsimportqthelpersasqthfromcdl.utils.miscimportgo_to_errorfromcdl.utils.qthelpersimport(add_corner_menu,bring_to_front,configure_menu_about_to_show,)fromcdl.widgetsimportinstconfviewer,logviewer,statusifTYPE_CHECKING:fromtypingimportLiteralfromcdl.core.gui.panel.baseimportAbstractPanel,BaseDataPanelfromcdl.core.gui.panel.imageimportImagePanelfromcdl.core.gui.panel.macroimportMacroPanelfromcdl.core.gui.panel.signalimportSignalPanelfromcdl.pluginsimportPluginBasedefremote_controlled(func):"""Decorator for remote-controlled methods"""@functools.wraps(func)defmethod_wrapper(*args,**kwargs):"""Decorator wrapper function"""win=args[0]# extracting 'self' from method argumentsalready_busy=notwin.ready_flagwin.ready_flag=Falsetry:output=func(*args,**kwargs)finally:ifnotalready_busy:win.SIG_READY.emit()win.ready_flag=TrueQW.QApplication.processEvents()returnoutputreturnmethod_wrapperclassCDLMainWindowMeta(type(QW.QMainWindow),abc.ABCMeta):"""Mixed metaclass to avoid conflicts"""
[docs]classCDLMainWindow(QW.QMainWindow,AbstractCDLControl,metaclass=CDLMainWindowMeta):"""DataLab main window Args: console: enable internal console hide_on_close: True to hide window on close """__instance=NoneSIG_READY=QC.Signal()SIG_SEND_OBJECT=QC.Signal(object)SIG_SEND_OBJECTLIST=QC.Signal(object)SIG_CLOSING=QC.Signal()
def__init__(self,console=None,hide_on_close=False):"""Initialize main window"""CDLMainWindow.__instance=selfsuper().__init__()self.setObjectName(APP_NAME)self.setWindowIcon(get_icon("DataLab.svg"))execenv.log(self,"Starting initialization")self.ready_flag=Trueself.hide_on_close=hide_on_closeself.__old_size:tuple[int,int]|None=Noneself.__memory_warning=Falseself.memorystatus:status.MemoryStatus|None=Noneself.console:DockableConsole|None=Noneself.macropanel:MacroPanel|None=Noneself.main_toolbar:QW.QToolBar|None=Noneself.signalpanel_toolbar:QW.QToolBar|None=Noneself.imagepanel_toolbar:QW.QToolBar|None=Noneself.signalpanel:SignalPanel|None=Noneself.imagepanel:ImagePanel|None=Noneself.tabwidget:QW.QTabWidget|None=Noneself.tabmenu:QW.QMenu|None=Noneself.docks:dict[AbstractPanel,QW.QDockWidget]|None=Noneself.h5inputoutput=H5InputOutput(self)self.openh5_action:QW.QAction|None=Noneself.saveh5_action:QW.QAction|None=Noneself.browseh5_action:QW.QAction|None=Noneself.settings_action:QW.QAction|None=Noneself.quit_action:QW.QAction|None=Noneself.autorefresh_action:QW.QAction|None=Noneself.showfirstonly_action:QW.QAction|None=Noneself.showlabel_action:QW.QAction|None=Noneself.file_menu:QW.QMenu|None=Noneself.edit_menu:QW.QMenu|None=Noneself.operation_menu:QW.QMenu|None=Noneself.processing_menu:QW.QMenu|None=Noneself.analysis_menu:QW.QMenu|None=Noneself.plugins_menu:QW.QMenu|None=Noneself.view_menu:QW.QMenu|None=Noneself.help_menu:QW.QMenu|None=Noneself.__update_color_mode(startup=True)self.__is_modified=Falseself.set_modified(False)# Starting XML-RPC server threadself.remote_server=RemoteServer(self)ifConf.main.rpc_server_enabled.get():self.remote_server.SIG_SERVER_PORT.connect(self.xmlrpc_server_started)self.remote_server.start()# Setup actions and menusifconsoleisNone:console=Conf.console.console_enabled.get()self.setup(console)self.__restore_pos_and_size()execenv.log(self,"Initialization done")# ------API related to XML-RPC remote control
[docs]@staticmethoddefxmlrpc_server_started(port):"""XML-RPC server has started, writing comm port in configuration file"""Conf.main.rpc_server_port.set(port)
def__get_current_basedatapanel(self)->BaseDataPanel:"""Return the current BaseDataPanel, or the signal panel if macro panel is active Returns: BaseDataPanel: current panel """panel=self.tabwidget.currentWidget()ifnotisinstance(panel,base.BaseDataPanel):panel=self.signalpanelreturnpaneldef__get_datapanel(self,panel:str|None)->BaseDataPanel:"""Return a specific BaseDataPanel. Args: panel: panel name (valid values: "signal", "image"). If None, current panel is used. Returns: Panel widget Raises: ValueError: if panel is unknown """ifnotpanel:returnself.__get_current_basedatapanel()ifpanel=="signal":returnself.signalpanelifpanel=="image":returnself.imagepanelraiseValueError(f"Unknown panel: {panel}")
[docs]@remote_controlleddefget_group_titles_with_object_infos(self,)->tuple[list[str],list[list[str]],list[list[str]]]:"""Return groups titles and lists of inner objects uuids and titles. Returns: Tuple: groups titles, lists of inner objects uuids and titles """panel=self.__get_current_basedatapanel()returnpanel.objmodel.get_group_titles_with_object_infos()
[docs]@remote_controlleddefget_object_titles(self,panel:str|None=None)->list[str]:"""Get object (signal/image) list for current panel. Objects are sorted by group number and object index in group. Args: panel: panel name (valid values: "signal", "image", "macro"). If None, current data panel is used (i.e. signal or image panel). Returns: List of object titles Raises: ValueError: if panel is unknown """ifnotpanelorpanelin("signal","image"):returnself.__get_datapanel(panel).objmodel.get_object_titles()ifpanel=="macro":returnself.macropanel.get_macro_titles()raiseValueError(f"Unknown panel: {panel}")
[docs]@remote_controlleddefget_object(self,nb_id_title:int|str|None=None,panel:str|None=None,)->SignalObj|ImageObj:"""Get object (signal/image) from index. Args: nb_id_title: Object number, or object id, or object title. Defaults to None (current object). panel: Panel name. Defaults to None (current panel). Returns: Object Raises: KeyError: if object not found TypeError: if index_id_title type is invalid """panelw=self.__get_datapanel(panel)ifnb_id_titleisNone:returnpanelw.objview.get_current_object()ifisinstance(nb_id_title,int):returnpanelw.objmodel.get_object_from_number(nb_id_title)ifisinstance(nb_id_title,str):try:returnpanelw.objmodel[nb_id_title]exceptKeyError:try:returnpanelw.objmodel.get_object_from_title(nb_id_title)exceptKeyErrorasexc:raiseKeyError(f"Invalid object index, id or title: {nb_id_title}")fromexcraiseTypeError(f"Invalid index_id_title type: {type(nb_id_title)}")
[docs]@remote_controlleddefget_object_uuids(self,panel:str|None=None)->list[str]:"""Get object (signal/image) uuid list for current panel. Objects are sorted by group number and object index in group. Args: panel (str | None): panel name (valid values: "signal", "image"). If None, current panel is used. Returns: list[str]: list of object uuids Raises: ValueError: if panel is unknown """returnself.__get_datapanel(panel).objmodel.get_object_ids()
[docs]@remote_controlleddefget_sel_object_uuids(self,include_groups:bool=False)->list[str]:"""Return selected objects uuids. Args: include_groups: If True, also return objects from selected groups. Returns: List of selected objects uuids. """panel=self.__get_current_basedatapanel()returnpanel.objview.get_sel_object_uuids(include_groups)
[docs]@remote_controlleddefselect_objects(self,selection:list[int|str],panel:str|None=None,)->None:"""Select objects in current panel. Args: selection: List of object numbers (1 to N) or uuids to select panel: panel name (valid values: "signal", "image"). If None, current panel is used. Defaults to None. """panel=self.__get_datapanel(panel)panel.objview.select_objects(selection)
[docs]@remote_controlleddefselect_groups(self,selection:list[int|str]|None=None,panel:str|None=None)->None:"""Select groups in current panel. Args: selection: List of group numbers (1 to N), or list of group uuids, or None to select all groups. Defaults to None. panel (str | None): panel name (valid values: "signal", "image"). If None, current panel is used. Defaults to None. """panel=self.__get_datapanel(panel)panel.objview.select_groups(selection)
[docs]@remote_controlleddefdelete_metadata(self,refresh_plot:bool=True,keep_roi:bool=False)->None:"""Delete metadata of selected objects Args: refresh_plot: Refresh plot. Defaults to True. keep_roi: Keep ROI. Defaults to False. """panel=self.__get_current_basedatapanel()panel.delete_metadata(refresh_plot,keep_roi)
[docs]@remote_controlleddefget_object_shapes(self,nb_id_title:int|str|None=None,panel:str|None=None,)->list:"""Get plot item shapes associated to object (signal/image). Args: nb_id_title: Object number, or object id, or object title. Defaults to None (current object). panel: Panel name. Defaults to None (current panel). Returns: List of plot item shapes """obj=self.get_object(nb_id_title,panel)returnlist(obj.iterate_shape_items(editable=False))
[docs]@remote_controlleddefadd_annotations_from_items(self,items:list,refresh_plot:bool=True,panel:str|None=None)->None:"""Add object annotations (annotation plot items). Args: items (list): annotation plot items refresh_plot (bool | None): refresh plot. Defaults to True. panel (str | None): panel name (valid values: "signal", "image"). If None, current panel is used. """panel=self.__get_datapanel(panel)panel.add_annotations_from_items(items,refresh_plot)
[docs]@remote_controlleddefadd_label_with_title(self,title:str|None=None,panel:str|None=None)->None:"""Add a label with object title on the associated plot Args: title (str | None): Label title. Defaults to None. If None, the title is the object title. panel (str | None): panel name (valid values: "signal", "image"). If None, current panel is used. """self.__get_datapanel(panel).add_label_with_title(title)
[docs]@remote_controlleddefrun_macro(self,number_or_title:int|str|None=None)->None:"""Run macro. Args: number: Number of the macro (starting at 1). Defaults to None (run current macro, or does nothing if there is no macro). """self.macropanel.run_macro(number_or_title)
[docs]@remote_controlleddefstop_macro(self,number_or_title:int|str|None=None)->None:"""Stop macro. Args: number: Number of the macro (starting at 1). Defaults to None (stop current macro, or does nothing if there is no macro). """self.macropanel.stop_macro(number_or_title)
[docs]@remote_controlleddefimport_macro_from_file(self,filename:str)->None:"""Import macro from file Args: filename: Filename. """self.macropanel.import_macro_from_file(filename)
# ------Misc.@propertydefpanels(self)->tuple[AbstractPanel,...]:"""Return the tuple of implemented panels (signal, image) Returns: tuple[SignalPanel, ImagePanel, MacroPanel]: tuple of panels """return(self.signalpanel,self.imagepanel,self.macropanel)def__set_low_memory_state(self,state:bool)->None:"""Set memory warning state"""self.__memory_warning=state
[docs]defconfirm_memory_state(self)->bool:# pragma: no cover"""Check memory warning state and eventually show a warning dialog Returns: bool: True if memory state is ok """ifnotenv.execenv.unattendedandself.__memory_warning:threshold=Conf.main.available_memory_threshold.get()answer=QW.QMessageBox.critical(self,_("Warning"),_("Available memory is below %d MB.<br><br>Do you want to continue?")%threshold,QW.QMessageBox.Yes|QW.QMessageBox.No,)returnanswer==QW.QMessageBox.YesreturnTrue
[docs]defcheck_stable_release(self)->None:# pragma: no cover"""Check if this is a stable release"""ifcdl.__version__.replace(".","").isdigit():# This is a stable releasereturnif"b"incdl.__version__:# This is a beta releaserel=_("This software is in the <b>beta stage</b> of its release cycle. ""The focus of beta testing is providing a feature complete ""software for users interested in trying new features before ""the final release. However, <u>beta software may not behave as ""expected and will probably have more bugs or performance issues ""than completed software</u>.")else:# This is an alpha releaserel=_("This software is in the <b>alpha stage</b> of its release cycle. ""The focus of alpha testing is providing an incomplete software ""for early testing of specific features by users. ""Please note that <u>alpha software was not thoroughly tested</u> ""by the developer before it is released.")txtlist=[f"<b>{APP_NAME}</b> v{cdl.__version__}:","",_("<i>This is not a stable release.</i>"),"",rel,]ifnotenv.execenv.unattended:QW.QMessageBox.warning(self,APP_NAME,"<br>".join(txtlist),QW.QMessageBox.Ok)
def__check_dependencies(self)->None:# pragma: no cover"""Check dependencies"""ifIS_FROZENorexecenv.unattended:# No need to check dependencies if DataLab has been frozen, or if# the user has chosen to ignore this check, or if we are in unattended mode# (i.e. running automated tests)ifIS_FROZEN:QW.QMessageBox.information(self,_("Information"),_("The dependency check feature is not relevant for the ""standalone version of DataLab."),QW.QMessageBox.Ok,)returntry:state=dephash.check_dependencies_hash(DATAPATH)bad_deps=[namefornameinstateifnotstate[name]]ifnotbad_deps:# Everything is OKQW.QMessageBox.information(self,_("Information"),_("All critical dependencies of DataLab have been qualified ""on this operating system."),QW.QMessageBox.Ok,)returnexceptIOError:bad_deps=Nonetxt0=_("Non-compliant dependency:")ifbad_depsisNoneorlen(bad_deps)>1:txt0=_("Non-compliant dependencies:")ifbad_depsisNone:txtlist=[_("DataLab has not yet been qualified on your operating system."),]else:txtlist=["<u>"+txt0+"</u> "+", ".join(bad_deps),"",_("At least one dependency does not comply with DataLab ""qualification standard reference (wrong dependency version ""has been installed, or dependency source code has been ""modified, or the application has not yet been qualified ""on your operating system)."),]txtlist+=["",_("This means that the application has not been officially qualified ""in this context and may not behave as expected."),]txt="<br>".join(txtlist)QW.QMessageBox.warning(self,APP_NAME,txt,QW.QMessageBox.Ok)
[docs]defcheck_for_previous_crash(self)->None:# pragma: no cover"""Check for previous crash"""ifexecenv.unattendedandnotexecenv.do_not_quit:# Showing the log viewer for testing purpose (unattended mode) but only# if option 'do_not_quit' is not set, to avoid blocking the test suiteself.__show_logviewer()elifConf.main.faulthandler_log_available.get(False)orConf.main.traceback_log_available.get(False):txt="<br>".join([logviewer.get_log_prompt_message(),"",_("Do you want to see available log files?"),])btns=QW.QMessageBox.StandardButton.Yes|QW.QMessageBox.StandardButton.Nochoice=QW.QMessageBox.warning(self,APP_NAME,txt,btns)ifchoice==QW.QMessageBox.StandardButton.Yes:self.__show_logviewer()
[docs]deftake_screenshot(self,name:str)->None:# pragma: no cover"""Take main window screenshot"""self.memorystatus.set_demo_mode(True)qth.grab_save_window(self,f"{name}")self.memorystatus.set_demo_mode(False)
[docs]deftake_menu_screenshots(self)->None:# pragma: no cover"""Take menu screenshots"""forpanelinself.panels:ifisinstance(panel,base.BaseDataPanel):self.tabwidget.setCurrentWidget(panel)fornamein("file","edit","view","operation","processing","analysis","help",):menu=getattr(self,f"{name}_menu")menu.popup(self.pos())qth.grab_save_window(menu,f"{panel.objectName()}_{name}")menu.close()
# ------GUI setupdef__restore_pos_and_size(self)->None:"""Restore main window position and size from configuration"""pos=Conf.main.window_position.get(None)ifposisnotNone:posx,posy=posself.move(QC.QPoint(posx,posy))size=Conf.main.window_size.get(None)ifsizeisNone:size=1200,700width,height=sizeself.resize(QC.QSize(width,height))ifposisnotNoneandsizeisnotNone:sgeo=self.screen().availableGeometry()out_inf=posx<-int(0.9*width)orposy<-int(0.9*height)out_sup=posx>int(0.9*sgeo.width())orposy>int(0.9*sgeo.height())iflen(QW.QApplication.screens())==1and(out_inforout_sup):# Main window is offscreenposx=min(max(posx,0),sgeo.width()-width)posy=min(max(posy,0),sgeo.height()-height)self.move(QC.QPoint(posx,posy))def__restore_state(self)->None:"""Restore main window state from configuration"""state=Conf.main.window_state.get(None)ifstateisnotNone:state=base64.b64decode(state)self.restoreState(QC.QByteArray(state))forwidgetinself.children():ifisinstance(widget,QW.QDockWidget):self.restoreDockWidget(widget)def__save_pos_size_and_state(self)->None:"""Save main window position, size and state to configuration"""is_maximized=self.windowState()==QC.Qt.WindowMaximizedConf.main.window_maximized.set(is_maximized)ifnotis_maximized:size=self.size()Conf.main.window_size.set((size.width(),size.height()))pos=self.pos()Conf.main.window_position.set((pos.x(),pos.y()))# Encoding window state into base64 string to avoid sending binary data# to the configuration file:state=base64.b64encode(self.saveState().data()).decode("ascii")Conf.main.window_state.set(state)
[docs]defsetup(self,console:bool=False)->None:"""Setup main window Args: console: True to setup console """self.__register_plugins()self.__configure_statusbar()self.__setup_global_actions()self.__add_signal_image_panels()self.__create_plugins_actions()self.__setup_central_widget()self.__add_menus()ifconsole:self.__setup_console()self.__update_actions(update_other_data_panel=True)self.__add_macro_panel()self.__configure_panels()# Now that everything is set up, we can restore the window state:self.__restore_state()
def__register_plugins(self)->None:"""Register plugins"""withqth.try_or_log_error("Discovering plugins"):# Discovering pluginsplugin_nb=len(discover_plugins())execenv.log(self,f"{plugin_nb} plugin(s) found")forplugin_classinPluginRegistry.get_plugin_classes():withqth.try_or_log_error(f"Instantiating plugin {plugin_class.__name__}"):# Instantiating pluginplugin:PluginBase=plugin_class()withqth.try_or_log_error(f"Registering plugin {plugin.info.name}"):# Registering pluginplugin.register(self)def__create_plugins_actions(self)->None:"""Create plugins actions"""withself.signalpanel.acthandler.new_category(ActionCategory.PLUGINS):withself.imagepanel.acthandler.new_category(ActionCategory.PLUGINS):forplugininPluginRegistry.get_plugins():withqth.try_or_log_error(f"Create actions for {plugin.info.name}"):plugin.create_actions()@staticmethoddef__unregister_plugins()->None:"""Unregister plugins"""whilePluginRegistry.get_plugins():# Unregistering pluginplugin=PluginRegistry.get_plugins()[-1]withqth.try_or_log_error(f"Unregistering plugin {plugin.info.name}"):plugin.unregister()def__configure_statusbar(self)->None:"""Configure status bar"""self.statusBar().showMessage(_("Welcome to %s!")%APP_NAME,5000)# Plugin statuspluginstatus=status.PluginStatus()self.statusBar().addPermanentWidget(pluginstatus)# XML-RPC server statusxmlrpcstatus=status.XMLRPCStatus()xmlrpcstatus.set_port(self.remote_server.port)self.statusBar().addPermanentWidget(xmlrpcstatus)# Memory statusthreshold=Conf.main.available_memory_threshold.get()self.memorystatus=status.MemoryStatus(threshold)self.memorystatus.SIG_MEMORY_ALARM.connect(self.__set_low_memory_state)self.statusBar().addPermanentWidget(self.memorystatus)def__add_toolbar(self,title:str,position:Literal["top","bottom","left","right"],name:str)->QW.QToolBar:"""Add toolbar to main window Args: title: toolbar title position: toolbar position name: toolbar name (Qt object name) """toolbar=QW.QToolBar(title,self)toolbar.setObjectName(name)area=getattr(QC.Qt,f"{position.capitalize()}ToolBarArea")self.addToolBar(area,toolbar)returntoolbardef__setup_global_actions(self)->None:"""Setup global actions"""self.openh5_action=create_action(self,_("Open HDF5 files..."),icon=get_icon("fileopen_h5.svg"),tip=_("Open one or several HDF5 files"),triggered=lambdachecked=False:self.open_h5_files(import_all=True),)self.saveh5_action=create_action(self,_("Save to HDF5 file..."),icon=get_icon("filesave_h5.svg"),tip=_("Save to HDF5 file"),triggered=self.save_to_h5_file,)self.browseh5_action=create_action(self,_("Browse HDF5 file..."),icon=get_icon("h5browser.svg"),tip=_("Browse an HDF5 file"),triggered=lambdachecked=False:self.open_h5_files(import_all=None),)self.settings_action=create_action(self,_("Settings..."),icon=get_icon("libre-gui-settings.svg"),tip=_("Open settings dialog"),triggered=self.__edit_settings,)self.main_toolbar=self.__add_toolbar(_("Main Toolbar"),"left","main_toolbar")add_actions(self.main_toolbar,[self.openh5_action,self.saveh5_action,self.browseh5_action,None,self.settings_action,],)# Quit action for "File menu" (added when populating menu on demand)ifself.hide_on_close:quit_text=_("Hide window")quit_tip=_("Hide DataLab window")else:quit_text=_("Quit")quit_tip=_("Quit application")ifsys.platform!="darwin":# On macOS, the "Quit" action is automatically added to the application menuself.quit_action=create_action(self,quit_text,shortcut=QG.QKeySequence(QG.QKeySequence.Quit),icon=get_icon("libre-gui-close.svg"),tip=quit_tip,triggered=self.close,)# View menu actionsself.autorefresh_action=create_action(self,_("Auto-refresh"),icon=get_icon("refresh-auto.svg"),tip=_("Auto-refresh plot when object is modified, added or removed"),toggled=self.toggle_auto_refresh,)self.showfirstonly_action=create_action(self,_("Show first object only"),icon=get_icon("show_first.svg"),tip=_("Show only the first selected object (signal or image)"),toggled=self.toggle_show_first_only,)self.showlabel_action=create_action(self,_("Show graphical object titles"),icon=get_icon("show_titles.svg"),tip=_("Show or hide ROI and other graphical object titles or subtitles"),toggled=self.toggle_show_titles,)def__add_signal_panel(self)->None:"""Setup signal toolbar, widgets and panel"""self.signalpanel_toolbar=self.__add_toolbar(_("Signal Panel Toolbar"),"left","signalpanel_toolbar")dpw=DockablePlotWidget(self,PlotType.CURVE)self.signalpanel=signal.SignalPanel(self,dpw,self.signalpanel_toolbar)self.signalpanel.SIG_STATUS_MESSAGE.connect(self.statusBar().showMessage)plot=dpw.get_plot()plot.add_item(make.legend("TR"))plot.SIG_ITEM_PARAMETERS_CHANGED.connect(self.signalpanel.plot_item_parameters_changed)plot.SIG_ITEM_MOVED.connect(self.signalpanel.plot_item_moved)returndpwdef__add_image_panel(self)->None:"""Setup image toolbar, widgets and panel"""self.imagepanel_toolbar=self.__add_toolbar(_("Image Panel Toolbar"),"left","imagepanel_toolbar")dpw=DockablePlotWidget(self,PlotType.IMAGE)self.imagepanel=image.ImagePanel(self,dpw,self.imagepanel_toolbar)# -----------------------------------------------------------------------------# # Before eventually disabling the "peritem" mode by default, wait for the# # plotpy bug to be fixed (peritem mode is not compatible with multiple image# # items):# for cspanel in (# self.imagepanel.plotwidget.get_xcs_panel(),# self.imagepanel.plotwidget.get_ycs_panel(),# ):# cspanel.peritem_ac.setChecked(False)# -----------------------------------------------------------------------------self.imagepanel.SIG_STATUS_MESSAGE.connect(self.statusBar().showMessage)plot=dpw.get_plot()plot.SIG_ITEM_PARAMETERS_CHANGED.connect(self.imagepanel.plot_item_parameters_changed)plot.SIG_ITEM_MOVED.connect(self.imagepanel.plot_item_moved)plot.SIG_LUT_CHANGED.connect(self.imagepanel.plot_lut_changed)returndpwdef__update_tab_menu(self)->None:"""Update tab menu"""current_panel:BaseDataPanel=self.tabwidget.currentWidget()add_actions(self.tabmenu,current_panel.get_context_menu().actions())def__add_signal_image_panels(self)->None:"""Add signal and image panels"""self.tabwidget=QW.QTabWidget()self.tabmenu=add_corner_menu(self.tabwidget)configure_menu_about_to_show(self.tabmenu,self.__update_tab_menu)cdock=self.__add_dockwidget(self.__add_signal_panel(),title=_("Signal View"))idock=self.__add_dockwidget(self.__add_image_panel(),title=_("Image View"))self.tabifyDockWidget(cdock,idock)self.docks={self.signalpanel:cdock,self.imagepanel:idock}self.tabwidget.currentChanged.connect(self.__tab_index_changed)self.signalpanel.SIG_OBJECT_ADDED.connect(lambda:self.set_current_panel("signal"))self.imagepanel.SIG_OBJECT_ADDED.connect(lambda:self.set_current_panel("image"))forpanelin(self.signalpanel,self.imagepanel):panel.setup_panel()def__setup_central_widget(self)->None:"""Setup central widget (main panel)"""self.tabwidget.setMaximumWidth(500)self.tabwidget.addTab(self.signalpanel,get_icon("signal.svg"),_("Signal Panel"))self.tabwidget.addTab(self.imagepanel,get_icon("image.svg"),_("Image Panel"))self.setCentralWidget(self.tabwidget)@staticmethoddef__get_local_doc_path()->str|None:"""Return local documentation path, if it exists"""locale=QC.QLocale.system().name()forsuffixin("_"+locale[:2],"_en"):path=osp.join(DATAPATH,"doc",f"{APP_NAME}{suffix}.pdf")ifosp.isfile(path):returnpathreturnNonedef__add_menus(self)->None:"""Adding menus"""self.file_menu=self.menuBar().addMenu(_("File"))configure_menu_about_to_show(self.file_menu,self.__update_file_menu)self.edit_menu=self.menuBar().addMenu(_("&Edit"))self.operation_menu=self.menuBar().addMenu(_("Operations"))self.processing_menu=self.menuBar().addMenu(_("Processing"))self.analysis_menu=self.menuBar().addMenu(_("Analysis"))self.plugins_menu=self.menuBar().addMenu(_("Plugins"))self.view_menu=self.menuBar().addMenu(_("&View"))configure_menu_about_to_show(self.view_menu,self.__update_view_menu)self.help_menu=self.menuBar().addMenu("?")formenuin(self.edit_menu,self.operation_menu,self.processing_menu,self.analysis_menu,self.plugins_menu,):configure_menu_about_to_show(menu,self.__update_generic_menu)help_menu_actions=[create_action(self,_("Online documentation"),icon=get_icon("libre-gui-help.svg"),triggered=lambda:webbrowser.open(__docurl__),),]localdocpath=self.__get_local_doc_path()iflocaldocpathisnotNone:help_menu_actions+=[create_action(self,_("PDF documentation"),icon=get_icon("help_pdf.svg"),triggered=lambda:webbrowser.open(localdocpath),),]help_menu_actions+=[create_action(self,_("Tour")+"...",icon=get_icon("tour.svg"),triggered=self.show_tour,),create_action(self,_("Demo")+"...",icon=get_icon("play_demo.svg"),triggered=self.play_demo,),None,]ifTEST_SEGFAULT_ERROR:help_menu_actions+=[create_action(self,_("Test segfault/Python error"),triggered=self.test_segfault_error,)]help_menu_actions+=[create_action(self,_("Log files")+"...",icon=get_icon("logs.svg"),triggered=self.__show_logviewer,),create_action(self,_("Installation and configuration")+"...",icon=get_icon("libre-toolbox.svg"),triggered=lambda:instconfviewer.exec_cdl_installconfig_dialog(self),),None,create_action(self,_("Project home page"),icon=get_icon("libre-gui-globe.svg"),triggered=lambda:webbrowser.open(__homeurl__),),create_action(self,_("Bug report or feature request"),icon=get_icon("libre-gui-globe.svg"),triggered=lambda:webbrowser.open(__supporturl__),),create_action(self,_("Check critical dependencies..."),triggered=self.__check_dependencies,),create_action(self,_("About..."),icon=get_icon("libre-gui-about.svg"),triggered=self.__about,),]add_actions(self.help_menu,help_menu_actions)def__setup_console(self)->None:"""Add an internal console"""ns={"cdl":self,"np":np,"sps":sps,"spi":spi,"os":os,"sys":sys,"osp":osp,"time":time,}msg=("Welcome to DataLab console!\n""---------------------------\n""You can access the main window with the 'cdl' variable.\n""Example:\n"" o = cdl.get_object() # returns currently selected object\n"" o = cdl[1] # returns object number 1\n"" o = cdl['My image'] # returns object which title is 'My image'\n"" o.data # returns object data\n""Modules imported at startup: ""os, sys, os.path as osp, time, ""numpy as np, scipy.signal as sps, scipy.ndimage as spi")self.console=DockableConsole(self,namespace=ns,message=msg,debug=DEBUG)self.console.setMaximumBlockCount(Conf.console.max_line_count.get(5000))self.console.go_to_error.connect(go_to_error)console_dock=self.__add_dockwidget(self.console,_("Console"))console_dock.hide()self.console.interpreter.widget_proxy.sig_new_prompt.connect(lambdatxt:self.repopulate_panel_trees())def__add_macro_panel(self)->None:"""Add macro panel"""self.macropanel=macro.MacroPanel()mdock=self.__add_dockwidget(self.macropanel,_("Macro Panel"))self.docks[self.macropanel]=mdockself.tabifyDockWidget(self.docks[self.imagepanel],mdock)self.docks[self.signalpanel].raise_()def__configure_panels(self)->None:"""Configure panels"""# Connectings signalsforpanelinself.panels:panel.SIG_OBJECT_ADDED.connect(self.set_modified)panel.SIG_OBJECT_REMOVED.connect(self.set_modified)self.macropanel.SIG_OBJECT_MODIFIED.connect(self.set_modified)# Initializing common panel actionsself.autorefresh_action.setChecked(Conf.view.auto_refresh.get(True))self.showfirstonly_action.setChecked(Conf.view.show_first_only.get(False))self.showlabel_action.setChecked(Conf.view.show_label.get(False))# Restoring current tab from last sessiontab_idx=Conf.main.current_tab.get(None)iftab_idxisnotNone:self.tabwidget.setCurrentIndex(tab_idx)# Set focus on current panel, so that keyboard shortcuts work (Fixes #10)self.tabwidget.currentWidget().setFocus()
[docs]defset_process_isolation_enabled(self,state:bool)->None:"""Enable/disable process isolation Args: state (bool): True to enable process isolation """forprocessorin(self.imagepanel.processor,self.signalpanel.processor):processor.set_process_isolation_enabled(state)
# ------Remote control
[docs]@remote_controlleddefget_current_panel(self)->str:"""Return current panel name Returns: str: panel name (valid values: "signal", "image", "macro") """panel=self.tabwidget.currentWidget()dock=self.docks[panel]ifpanelisself.signalpanelanddock.isVisible():return"signal"ifpanelisself.imagepanelanddock.isVisible():return"image"return"macro"
[docs]@remote_controlleddefset_current_panel(self,panel:str)->None:"""Switch to panel. Args: panel (str): panel name (valid values: "signal", "image", "macro") Raises: ValueError: unknown panel """ifself.get_current_panel()==panel:ifpanelin("signal","image"):# Force tab index changed event to be sure that the dock associated# to the current panel is raisedself.__tab_index_changed(self.tabwidget.currentIndex())returnifpanel=="signal":self.tabwidget.setCurrentWidget(self.signalpanel)elifpanel=="image":self.tabwidget.setCurrentWidget(self.imagepanel)elifpanel=="macro":self.docks[self.macropanel].raise_()else:raiseValueError(f"Unknown panel {panel}")
[docs]@remote_controlleddefcalc(self,name:str,param:gds.DataSet|None=None)->None:"""Call compute function ``name`` in current panel's processor. Args: name: Compute function name param: Compute function parameter. Defaults to None. Raises: ValueError: unknown function """panels=[self.tabwidget.currentWidget()]panels.extend(self.panels)forpanelinpanels:ifisinstance(panel,base.BaseDataPanel):forfuncnamein(name,f"compute_{name}"):func=getattr(panel.processor,funcname,None)iffuncisnotNone:ifparamisNone:func()else:func(param)returnraiseValueError(f"Unknown function {name}")
# ------GUI refresh
[docs]defhas_objects(self)->bool:"""Return True if sig/ima panels have any object"""returnsum(len(panel)forpanelinself.panels)>0
def__add_dockwidget(self,child,title:str)->QW.QDockWidget:"""Add QDockWidget and toggleViewAction"""dockwidget,location=child.create_dockwidget(title)dockwidget.setObjectName(title)self.addDockWidget(location,dockwidget)returndockwidget
[docs]defrepopulate_panel_trees(self)->None:"""Repopulate all panel trees"""forpanelinself.panels:ifisinstance(panel,base.BaseDataPanel):panel.objview.populate_tree()
def__update_actions(self,update_other_data_panel:bool=False)->None:"""Update selection dependent actions Args: update_other_data_panel: True to update other data panel actions (i.e. if the current panel is the signal panel, also update the image panel actions, and vice-versa) """is_signal=self.tabwidget.currentWidget()isself.signalpanelpanel=self.signalpanelifis_signalelseself.imagepanelother_panel=self.imagepanelifis_signalelseself.signalpanelifupdate_other_data_panel:other_panel.selection_changed()panel.selection_changed()self.signalpanel_toolbar.setVisible(is_signal)self.imagepanel_toolbar.setVisible(notis_signal)ifself.plugins_menuisnotNone:plugin_actions=panel.get_category_actions(ActionCategory.PLUGINS)self.plugins_menu.setEnabled(len(plugin_actions)>0)def__tab_index_changed(self,index:int)->None:"""Switch from signal to image mode, or vice-versa"""dock=self.docks[self.tabwidget.widget(index)]dock.raise_()self.__update_actions()def__update_generic_menu(self,menu:QW.QMenu|None=None)->None:"""Update menu before showing up -- Generic method"""ifmenuisNone:menu=self.sender()menu.clear()panel=self.tabwidget.currentWidget()category={self.file_menu:ActionCategory.FILE,self.edit_menu:ActionCategory.EDIT,self.view_menu:ActionCategory.VIEW,self.operation_menu:ActionCategory.OPERATION,self.processing_menu:ActionCategory.PROCESSING,self.analysis_menu:ActionCategory.ANALYSIS,self.plugins_menu:ActionCategory.PLUGINS,}[menu]actions=panel.get_category_actions(category)add_actions(menu,actions)def__update_file_menu(self)->None:"""Update file menu before showing up"""self.saveh5_action.setEnabled(self.has_objects())self.__update_generic_menu(self.file_menu)add_actions(self.file_menu,[None,self.openh5_action,self.saveh5_action,self.browseh5_action,None,self.settings_action,],)ifself.quit_actionisnotNone:add_actions(self.file_menu,[None,self.quit_action])def__update_view_menu(self)->None:"""Update view menu before showing up"""self.__update_generic_menu(self.view_menu)add_actions(self.view_menu,[None]+self.createPopupMenu().actions())
[docs]@remote_controlleddeftoggle_show_titles(self,state:bool)->None:"""Toggle show annotations option Args: state: state """Conf.view.show_label.set(state)fordatapanelin(self.signalpanel,self.imagepanel):forobjindatapanel.objmodel:obj.set_metadata_option("showlabel",state)datapanel.SIG_REFRESH_PLOT.emit("selected",True)
[docs]@remote_controlleddeftoggle_auto_refresh(self,state:bool)->None:"""Toggle auto refresh option Args: state: state """Conf.view.auto_refresh.set(state)fordatapanelin(self.signalpanel,self.imagepanel):datapanel.plothandler.set_auto_refresh(state)
[docs]@remote_controlleddeftoggle_show_first_only(self,state:bool)->None:"""Toggle show first only option Args: state: state """Conf.view.show_first_only.set(state)fordatapanelin(self.signalpanel,self.imagepanel):datapanel.plothandler.set_show_first_only(state)
# ------Common features
[docs]@remote_controlleddefreset_all(self)->None:"""Reset all application data"""forpanelinself.panels:ifpanelisnotNone:panel.remove_all_objects()
@staticmethoddef__check_h5file(filename:str,operation:str)->str:"""Check HDF5 filename"""filename=osp.abspath(osp.normpath(filename))bname=osp.basename(filename)ifoperation=="load"andnotosp.isfile(filename):raiseIOError(f'File not found "{bname}"')Conf.main.base_dir.set(filename)returnfilename
[docs]@remote_controlleddefsave_to_h5_file(self,filename=None)->None:"""Save to a DataLab HDF5 file Args: filename (str): HDF5 filename. If None, a file dialog is opened. Raises: IOError: if filename is invalid or file cannot be saved. """iffilenameisNone:basedir=Conf.main.base_dir.get()withqth.save_restore_stds():filename,_fl=getsavefilename(self,_("Save"),basedir,"HDF5 (*.h5)")ifnotfilename:returnwithqth.qt_try_loadsave_file(self,filename,"save"):filename=self.__check_h5file(filename,"save")self.h5inputoutput.save_file(filename)self.set_modified(False)
[docs]@remote_controlleddefopen_h5_files(self,h5files:list[str]|None=None,import_all:bool|None=None,reset_all:bool|None=None,)->None:"""Open a DataLab HDF5 file or import from any other HDF5 file. Args: h5files: HDF5 filenames (optionally with dataset name, separated by ":") import_all (bool): Import all datasets from HDF5 files reset_all (bool): Reset all application data before importing Returns: None """ifnotself.confirm_memory_state():returnifreset_allisNone:reset_all=Falseifself.has_objects():answer=QW.QMessageBox.question(self,_("Warning"),_("Do you want to remove all signals and images ""before importing data from HDF5 files?"),QW.QMessageBox.Yes|QW.QMessageBox.No,)ifanswer==QW.QMessageBox.Yes:reset_all=Trueifh5filesisNone:basedir=Conf.main.base_dir.get()withqth.save_restore_stds():h5files,_fl=getopenfilenames(self,_("Open"),basedir,_("HDF5 files (*.h5 *.hdf5)"))ifnoth5files:returnfilenames,dsetnames=[],[]forfname_with_dsetinh5files:if","infname_with_dset:filename,dsetname=fname_with_dset.split(",")dsetnames.append(dsetname)else:filename=fname_with_dsetdsetnames.append(None)filenames.append(filename)ifimport_allisNoneandall(dsetnameisNonefordsetnameindsetnames):self.browse_h5_files(filenames,reset_all)returnforfilename,dsetnameinzip(filenames,dsetnames):ifimport_allisNoneanddsetnameisNone:self.import_h5_file(filename,reset_all)else:withqth.qt_try_loadsave_file(self,filename,"load"):filename=self.__check_h5file(filename,"load")ifdsetnameisNone:self.h5inputoutput.open_file(filename,import_all,reset_all)else:self.h5inputoutput.import_dataset_from_file(filename,dsetname)reset_all=False
[docs]defbrowse_h5_files(self,filenames:list[str],reset_all:bool)->None:"""Browse HDF5 files Args: filenames (list): HDF5 filenames reset_all (bool): Reset all application data before importing Returns: None """forfilenameinfilenames:self.__check_h5file(filename,"load")self.h5inputoutput.import_files(filenames,False,reset_all)
[docs]@remote_controlleddefimport_h5_file(self,filename:str,reset_all:bool|None=None)->None:"""Import HDF5 file into DataLab Args: filename (str): HDF5 filename (optionally with dataset name, separated by ":") reset_all (bool): Delete all DataLab signals/images before importing data Returns: None """withqth.qt_try_loadsave_file(self,filename,"load"):filename=self.__check_h5file(filename,"load")self.h5inputoutput.import_files([filename],False,reset_all)
# This method is intentionally *not* remote controlled# (see TODO regarding RemoteClient.add_object method)# @remote_controlled
[docs]defadd_object(self,obj:SignalObj|ImageObj)->None:"""Add object - signal or image Args: obj (SignalObj or ImageObj): object to add (signal or image) """ifself.confirm_memory_state():ifisinstance(obj,SignalObj):self.signalpanel.add_object(obj)elifisinstance(obj,ImageObj):self.imagepanel.add_object(obj)else:raiseTypeError(f"Unsupported object type {type(obj)}")
[docs]@remote_controlleddefload_from_files(self,filenames:list[str])->None:"""Open objects from files in current panel (signals/images) Args: filenames: list of filenames """panel=self.__get_current_basedatapanel()panel.load_from_files(filenames)
# ------Other methods related to AbstractCDLControl interface
[docs]defget_version(self)->str:"""Return DataLab public version. Returns: str: DataLab version """returncdl.__version__
[docs]defadd_signal(self,title:str,xdata:np.ndarray,ydata:np.ndarray,xunit:str|None=None,yunit:str|None=None,xlabel:str|None=None,ylabel:str|None=None,)->bool:# pylint: disable=too-many-arguments"""Add signal data to DataLab. Args: title (str): Signal title xdata (numpy.ndarray): X data ydata (numpy.ndarray): Y data xunit (str | None): X unit. Defaults to None. yunit (str | None): Y unit. Defaults to None. xlabel (str | None): X label. Defaults to None. ylabel (str | None): Y label. Defaults to None. Returns: bool: True if signal was added successfully, False otherwise Raises: ValueError: Invalid xdata dtype ValueError: Invalid ydata dtype """obj=create_signal(title,xdata,ydata,units=(xunit,yunit),labels=(xlabel,ylabel),)self.add_object(obj)returnTrue
[docs]defadd_image(self,title:str,data:np.ndarray,xunit:str|None=None,yunit:str|None=None,zunit:str|None=None,xlabel:str|None=None,ylabel:str|None=None,zlabel:str|None=None,)->bool:# pylint: disable=too-many-arguments"""Add image data to DataLab. Args: title (str): Image title data (numpy.ndarray): Image data xunit (str | None): X unit. Defaults to None. yunit (str | None): Y unit. Defaults to None. zunit (str | None): Z unit. Defaults to None. xlabel (str | None): X label. Defaults to None. ylabel (str | None): Y label. Defaults to None. zlabel (str | None): Z label. Defaults to None. Returns: bool: True if image was added successfully, False otherwise Raises: ValueError: Invalid data dtype """obj=create_image(title,data,units=(xunit,yunit,zunit),labels=(xlabel,ylabel,zlabel),)self.add_object(obj)returnTrue
[docs]defclose_properly(self)->bool:"""Close properly Returns: bool: True if closed properly, False otherwise """ifnotenv.execenv.unattendedandself.__is_modified:answer=QW.QMessageBox.warning(self,_("Quit"),_("Do you want to save all signals and images ""to an HDF5 file before quitting DataLab?"),QW.QMessageBox.Yes|QW.QMessageBox.No|QW.QMessageBox.Cancel,)ifanswer==QW.QMessageBox.Yes:self.save_to_h5_file()ifself.__is_modified:returnFalseelifanswer==QW.QMessageBox.Cancel:returnFalseself.hide()# Avoid showing individual widgets closing one after the otherforpanelinself.panels:ifpanelisnotNone:panel.close()ifself.consoleisnotNone:try:self.console.close()exceptRuntimeError:# TODO: [P3] Investigate further why the following error occurs when# restarting the mainwindow (this is *not* a production case):# "RuntimeError: wrapped C/C++ object of type DockableConsole# has been deleted".# Another solution to avoid this error would be to really restart# the application (run each unit test in a separate process), but# it would represent too much effort for an error occuring in test# configurations only.passself.reset_all()self.__save_pos_size_and_state()self.__unregister_plugins()# Saving current tab for next sessionConf.main.current_tab.set(self.tabwidget.currentIndex())execenv.log(self,"closed properly")returnTrue