# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.""".. Base panel objects (see parent package :mod:`cdl.core.gui.panel`)"""# pylint: disable=invalid-name # Allows short reference names like x, y, ...from__future__importannotationsimportabcimportdataclassesimportos.pathasospimportreimportwarningsfromtypingimportTYPE_CHECKING,Generic,Typeimportguidata.datasetasgdsimportguidata.dataset.qtwidgetsasgdqimportnumpyasnpimportplotpy.iofromguidata.configtoolsimportget_iconfromguidata.datasetimportupdate_datasetfromguidata.ioimportJSONHandlerfromguidata.qthelpersimportadd_actions,create_action,exec_dialogfromguidata.widgets.arrayeditorimportArrayEditorfromplotpy.plotimportPlotDialogfromplotpy.toolsimportActionToolfromqtpyimportQtCoreasQCfromqtpyimportQtWidgetsasQWfromqtpy.compatimportgetopenfilename,getopenfilenames,getsavefilenamefromcdl.configimportAPP_NAME,Conf,_fromcdl.core.guiimportactionhandler,objectmodel,objectviewfromcdl.core.gui.roieditorimportTypeROIEditorfromcdl.core.model.baseimport(ResultProperties,ResultShape,TypeObj,TypeROI,items_to_json,)fromcdl.core.model.signalimportcreate_signalfromcdl.envimportexecenvfromcdl.utils.qthelpersimport(CallbackWorker,create_progress_bar,qt_long_callback,qt_try_except,qt_try_loadsave_file,save_restore_stds,)fromcdl.widgets.textimportimportTextImportWizardifTYPE_CHECKING:fromtypingimportCallablefromplotpy.itemsimportCurveItem,LabelItem,MaskedImageItemfromplotpy.tools.baseimportGuiToolfromcdl.core.guiimportObjItffromcdl.core.gui.mainimportCDLMainWindowfromcdl.core.gui.plothandlerimportImagePlotHandler,SignalPlotHandlerfromcdl.core.gui.processor.imageimportImageProcessorfromcdl.core.gui.processor.signalimportSignalProcessorfromcdl.core.io.imageimportImageIORegistryfromcdl.core.io.nativeimportNativeH5Reader,NativeH5Writerfromcdl.core.io.signalimportSignalIORegistryfromcdl.core.model.baseimportShapeTypesfromcdl.core.model.imageimportImageObj,NewImageParamfromcdl.core.model.signalimportNewSignalParam,SignalObj
[docs]defis_plot_item_serializable(item:ShapeTypes)->bool:"""Return True if plot item is serializable"""try:plotpy.io.item_class_from_name(item.__class__.__name__)returnTrueexceptAssertionError:returnFalse
[docs]defupdate_properties_from(self,param:SignalObj|ImageObj|None=None):"""Update properties from signal/image dataset"""self.properties.setDisabled(paramisNone)ifparamisNone:param=self.paramclass()self.properties.dataset.set_defaults()update_dataset(self.properties.dataset,param)self.properties.get()self.set_param_label(param)self.properties.apply_button.setEnabled(False)
[docs]classAbstractPanelMeta(type(QW.QSplitter),abc.ABCMeta):"""Mixed metaclass to avoid conflicts"""
[docs]classAbstractPanel(QW.QSplitter,metaclass=AbstractPanelMeta):"""Object defining DataLab panel interface, based on a vertical QSplitter widget A panel handle an object list (objects are signals, images, macros, ...). Each object must implement ``cdl.core.gui.ObjItf`` interface """H5_PREFIX=""SIG_OBJECT_ADDED=QC.Signal()SIG_OBJECT_REMOVED=QC.Signal()@abc.abstractmethoddef__init__(self,parent):super().__init__(QC.Qt.Vertical,parent)self.setObjectName(self.__class__.__name__[0].lower())# Check if the class implements __len__, __getitem__ and __iter__formethodin("__len__","__getitem__","__iter__"):ifnothasattr(self,method):raiseNotImplementedError(f"Class {self.__class__.__name__} must implement method {method}")# pylint: disable=unused-argument
[docs]defget_serializable_name(self,obj:ObjItf)->str:"""Return serializable name of object"""title=re.sub("[^-a-zA-Z0-9_.() ]+","",obj.title.replace("/","_"))name=f"{obj.short_id}: {title}"returnname
[docs]defserialize_object_to_hdf5(self,obj:ObjItf,writer:NativeH5Writer)->None:"""Serialize object to HDF5 file"""withwriter.group(self.get_serializable_name(obj)):obj.serialize(writer)
[docs]defdeserialize_object_from_hdf5(self,reader:NativeH5Reader,name:str)->ObjItf:"""Deserialize object from a HDF5 file"""withreader.group(name):obj=self.create_object()obj.deserialize(reader)obj.regenerate_uuid()returnobj
[docs]@abc.abstractmethoddefserialize_to_hdf5(self,writer:NativeH5Writer)->None:"""Serialize whole panel to a HDF5 file"""
[docs]@abc.abstractmethoddefdeserialize_from_hdf5(self,reader:NativeH5Reader)->None:"""Deserialize whole panel from a HDF5 file"""
[docs]@abc.abstractmethoddefcreate_object(self)->ObjItf:"""Create and return object"""
[docs]@abc.abstractmethoddefadd_object(self,obj:ObjItf)->None:"""Add object to panel"""
[docs]@abc.abstractmethoddefremove_all_objects(self):"""Remove all objects"""self.SIG_OBJECT_REMOVED.emit()
[docs]@dataclasses.dataclassclassResultData:"""Result data associated to a shapetype"""results:list[ResultShape|ResultProperties]=Nonexlabels:list[str]=Noneylabels:list[str]=None
[docs]defcreate_resultdata_dict(objs:list[SignalObj|ImageObj])->dict[str,ResultData]:"""Return result data dictionary Args: objs: List of objects Returns: Result data dictionary: keys are result categories, values are ResultData """rdatadict:dict[str,ResultData]={}forobjinobjs:forresultinlist(obj.iterate_resultshapes())+list(obj.iterate_resultproperties()):rdata=rdatadict.setdefault(result.category,ResultData([],None,[]))rdata.results.append(result)rdata.xlabels=result.headersfori_row_resinrange(result.array.shape[0]):ylabel=f"{result.title}({obj.short_id})"i_roi=int(result.array[i_row_res,0])ifi_roi>=0:ylabel+=f"|ROI{i_roi}"rdata.ylabels.append(ylabel)returnrdatadict
[docs]classBaseDataPanel(AbstractPanel,Generic[TypeObj,TypeROI,TypeROIEditor]):"""Object handling the item list, the selected item properties and plot"""PANEL_STR=""# e.g. "Signal Panel"PANEL_STR_ID=""# e.g. "signal"PARAMCLASS:TypeObj=None# Replaced in child objectANNOTATION_TOOLS=()MINDIALOGSIZE=(800,600)MAXDIALOGSIZE=0.95# % of DataLab's main window size# Replaced by the right class in child object:IO_REGISTRY:SignalIORegistry|ImageIORegistry|None=NoneSIG_STATUS_MESSAGE=QC.Signal(str)# emitted by "qt_try_except" decoratorSIG_REFRESH_PLOT=QC.Signal(str,bool)# Connected to PlotHandler.refresh_plotROIDIALOGOPTIONS={}
[docs]@staticmethod@abc.abstractmethoddefget_roieditor_class()->Type[TypeROIEditor]:"""Return ROI editor class"""
[docs]defplot_item_parameters_changed(self,item:CurveItem|MaskedImageItem|LabelItem)->None:"""Plot items changed: update metadata of all objects from plot items"""# Find the object corresponding to the plot itemobj=self.plothandler.get_obj_from_item(item)ifobjisnotNone:obj.update_metadata_from_plot_item(item)ifobjisself.objview.get_current_object():self.objprop.update_properties_from(obj)self.plothandler.update_resultproperty_from_plot_item(item)
[docs]defplot_item_moved(self,item:LabelItem,x0:float,# pylint: disable=unused-argumenty0:float,# pylint: disable=unused-argumentx1:float,# pylint: disable=unused-argumenty1:float,# pylint: disable=unused-argument)->None:"""Plot item moved: update metadata of all objects from plot items Args: item: Plot item x0: new x0 coordinate y0: new y0 coordinate x1: new x1 coordinate y1: new y1 coordinate """self.plothandler.update_resultproperty_from_plot_item(item)
[docs]defserialize_object_to_hdf5(self,obj:TypeObj,writer:NativeH5Writer)->None:"""Serialize object to HDF5 file"""# Before serializing, update metadata from plot item parameters, in order to# save the latest visualization settings:try:item=self.plothandler[obj.uuid]obj.update_metadata_from_plot_item(item)exceptKeyError:# Plot item has not been created yet (this happens when auto-refresh has# been disabled)passsuper().serialize_object_to_hdf5(obj,writer)
[docs]defserialize_to_hdf5(self,writer:NativeH5Writer)->None:"""Serialize whole panel to a HDF5 file"""withwriter.group(self.H5_PREFIX):forgroupinself.objmodel.get_groups():withwriter.group(self.get_serializable_name(group)):withwriter.group("title"):writer.write_str(group.title)forobjingroup.get_objects():self.serialize_object_to_hdf5(obj,writer)
[docs]defdeserialize_from_hdf5(self,reader:NativeH5Reader)->None:"""Deserialize whole panel from a HDF5 file"""withreader.group(self.H5_PREFIX):fornameinreader.h5.get(self.H5_PREFIX,[]):withreader.group(name):group=self.add_group("")withreader.group("title"):group.title=reader.read_str()forobj_nameinreader.h5.get(f"{self.H5_PREFIX}/{name}",[]):obj=self.deserialize_object_from_hdf5(reader,obj_name)self.add_object(obj,group.uuid,set_current=False)self.selection_changed()
def__len__(self)->int:"""Return number of objects"""returnlen(self.objmodel)def__getitem__(self,nb:int)->TypeObj:"""Return object from its number (1 to N)"""returnself.objmodel.get_object_from_number(nb)def__iter__(self):"""Iterate over objects"""returniter(self.objmodel)
[docs]defcreate_object(self)->TypeObj:"""Create object (signal or image) Returns: SignalObj or ImageObj object """returnself.PARAMCLASS()# pylint: disable=not-callable
[docs]@qt_try_except()defadd_object(self,obj:TypeObj,group_id:str|None=None,set_current:bool=True,)->None:"""Add object Args: obj: SignalObj or ImageObj object group_id: group id set_current: if True, set the added object as current """ifobjinself.objmodel:# Prevent adding the same object twiceraiseValueError(f"Object {hex(id(obj))} already in panel. "f"The same object cannot be added twice: "f"please use a copy of the object.")ifgroup_idisNone:group_id=self.objview.get_current_group_id()ifgroup_idisNone:groups=self.objmodel.get_groups()ifgroups:group_id=groups[0].uuidelse:group_id=self.add_group("").uuidobj.check_data()self.objmodel.add_object(obj,group_id)# Block signals to avoid updating the plot (unnecessary refresh)self.objview.blockSignals(True)self.objview.add_object_item(obj,group_id,set_current=set_current)self.objview.blockSignals(False)# Emit signal to ensure that the data panel is shown in the main window and# that the plot is updated (trigger a refresh of the plot)self.SIG_OBJECT_ADDED.emit()self.objview.update_tree()
[docs]defremove_all_objects(self)->None:"""Remove all objects"""# iterate over a copy of self.__separate_views dict keys to avoid RuntimeError:# dictionary changed size during iterationfordlginlist(self.__separate_views):dlg.done(QW.QDialog.DialogCode.Rejected)self.objmodel.clear()self.plothandler.clear()self.objview.populate_tree()self.SIG_REFRESH_PLOT.emit("selected",True)super().remove_all_objects()
# ---- Signal/Image Panel API ------------------------------------------------------
[docs]defget_category_actions(self,category:actionhandler.ActionCategory)->list[QW.QAction]:# pragma: no cover"""Return actions for category"""returnself.acthandler.feature_actions.get(category,[])
[docs]defget_context_menu(self)->QW.QMenu:"""Update and return context menu"""# Note: For now, this is completely unnecessary to clear context menu everytime,# but implementing it this way could be useful in the future in menu contents# should take into account current object selectionself.context_menu.clear()actions=self.get_category_actions(actionhandler.ActionCategory.CONTEXT_MENU)add_actions(self.context_menu,actions)returnself.context_menu
def__popup_contextmenu(self,position:QC.QPoint)->None:# pragma: no cover"""Popup context menu at position"""menu=self.get_context_menu()menu.popup(position)# ------Creating, adding, removing objects------------------------------------------
[docs]defduplicate_object(self)->None:"""Duplication signal/image object"""ifnotself.mainwindow.confirm_memory_state():return# Duplicate individual objects (exclusive with respect to groups)foroidinself.objview.get_sel_object_uuids():self.__duplicate_individual_obj(oid,set_current=False)# Duplicate groups (exclusive with respect to individual objects)forgroupinself.objview.get_sel_groups():new_group=self.add_group(group.title)foroidinself.objmodel.get_group_object_ids(group.uuid):self.__duplicate_individual_obj(oid,new_group.uuid,set_current=False)self.selection_changed(update_items=True)
[docs]defcopy_metadata(self)->None:"""Copy object metadata"""obj=self.objview.get_sel_objects()[0]self.__metadata_clipboard=obj.metadata.copy()new_pref=obj.short_id+"_"forkey,valueinobj.metadata.items():ifResultShape.match(key,value):mshape=ResultShape.from_metadata_entry(key,value)ifnotre.match(obj.PREFIX+r"[0-9]{3}[\s]*",mshape.title):# Handling additional result (e.g. diameter)fora_key,a_valueinobj.metadata.items():ifisinstance(a_key,str)anda_key.startswith(mshape.title):self.__metadata_clipboard.pop(a_key)self.__metadata_clipboard[new_pref+a_key]=a_valuemshape.title=new_pref+mshape.title# Handling result shapeself.__metadata_clipboard.pop(key)self.__metadata_clipboard[mshape.key]=value
[docs]defpaste_metadata(self)->None:"""Paste metadata to selected object(s)"""sel_objects=self.objview.get_sel_objects(include_groups=True)forobjinsorted(sel_objects,key=lambdaobj:obj.short_id,reverse=True):obj.metadata.update(self.__metadata_clipboard)self.SIG_REFRESH_PLOT.emit("selected",True)
[docs]defremove_object(self,force:bool=False)->None:"""Remove signal/image object Args: force: if True, remove object without confirmation. Defaults to False. """sel_groups=self.objview.get_sel_groups()ifsel_groupsandnotforceandnotexecenv.unattended:answer=QW.QMessageBox.warning(self,_("Delete group(s)"),_("Are you sure you want to delete the selected group(s)?"),QW.QMessageBox.Yes|QW.QMessageBox.No,)ifanswer==QW.QMessageBox.No:returnsel_objects=self.objview.get_sel_objects(include_groups=True)forobjinsorted(sel_objects,key=lambdaobj:obj.short_id,reverse=True):dlg_list:list[QW.QDialog]=[]fordlg,obj_iinself.__separate_views.items():ifobj_iisobj:dlg_list.append(dlg)fordlgindlg_list:dlg.done(QW.QDialog.DialogCode.Rejected)self.plothandler.remove_item(obj.uuid)self.objview.remove_item(obj.uuid,refresh=False)self.objmodel.remove_object(obj)forgroupinsel_groups:self.objview.remove_item(group.uuid,refresh=False)self.objmodel.remove_group(group)self.objview.update_tree()self.selection_changed(update_items=True)self.SIG_OBJECT_REMOVED.emit()
[docs]defdelete_all_objects(self)->None:# pragma: no cover"""Confirm before removing all objects"""iflen(self)==0:returnanswer=QW.QMessageBox.warning(self,_("Delete all"),_("Do you want to delete all objects (%s)?")%self.PANEL_STR,QW.QMessageBox.Yes|QW.QMessageBox.No,)ifanswer==QW.QMessageBox.Yes:self.remove_all_objects()
[docs]defdelete_metadata(self,refresh_plot:bool=True,keep_roi:bool|None=None)->None:"""Delete metadata of selected objects Args: refresh_plot: Refresh plot. Defaults to True. keep_roi: Keep regions of interest, if any. Defaults to None (ask user). """sel_objs=self.objview.get_sel_objects(include_groups=True)# Check if there are regions of interest first:roi_backup:dict[TypeObj,np.ndarray]={}ifany(obj.roiisnotNoneforobjinsel_objs):ifexecenv.unattendedandkeep_roiisNone:keep_roi=Falseelifkeep_roiisNone:answer=QW.QMessageBox.warning(self,_("Delete metadata"),_("Some selected objects have regions of interest.<br>""Do you want to delete them as well?"),QW.QMessageBox.Yes|QW.QMessageBox.No|QW.QMessageBox.Cancel,)ifanswer==QW.QMessageBox.Cancel:returnkeep_roi=answer==QW.QMessageBox.Noifkeep_roi:forobjinsel_objs:ifobj.roiisnotNone:roi_backup[obj]=obj.roi# Delete metadata:forindex,objinenumerate(sel_objs):obj.reset_metadata_to_defaults()ifnotkeep_roi:obj.invalidate_maskdata_cache()ifobjinroi_backup:obj.roi=roi_backup[obj]ifindex==0:self.selection_changed()ifrefresh_plot:self.SIG_REFRESH_PLOT.emit("selected",True)
[docs]defcopy_titles_to_clipboard(self)->None:"""Copy object titles to clipboard (for reproducibility)"""QW.QApplication.clipboard().setText(str(self.objview))
[docs]defnew_group(self)->None:"""Create a new group"""# Open a message box to enter the group namegroup_name,ok=QW.QInputDialog.getText(self,_("New group"),_("Group name:"))ifok:self.add_group(group_name)
[docs]defrename_group(self,new_name:str|None=None)->None:"""Rename a group Args: new_name: new group name. Defaults to None (ask user). """sel_groups=self.objview.get_sel_groups()ifnotsel_groupsorlen(sel_groups)>1:# Won't happen in the application, but could happen in tests or using the# API directlyraiseValueError("Select one group to rename")group=sel_groups[0]ifnew_nameisNone:new_name,ok=QW.QInputDialog.getText(self,_("Rename group"),_("Group name:"),QW.QLineEdit.Normal,group.title,)ifnotok:returngroup.title=new_nameself.objview.update_item(group.uuid)
[docs]@abc.abstractmethoddefget_newparam_from_current(self,newparam:NewSignalParam|NewImageParam|None=None)->NewSignalParam|NewImageParam|None:"""Get new object parameters from the current object. Args: newparam: new object parameters. If None, create a new one. Returns: New object parameters """
[docs]@abc.abstractmethoddefnew_object(self,newparam:NewSignalParam|NewImageParam|None=None,addparam:gds.DataSet|None=None,edit:bool=True,add_to_panel:bool=True,)->TypeObj|None:"""Create a new object (signal/image). Args: newparam: new object parameters addparam: additional parameters edit: Open a dialog box to edit parameters (default: True) add_to_panel: Add object to panel (default: True) Returns: New object """
[docs]defset_current_object_title(self,title:str)->None:"""Set current object title"""obj=self.objview.get_current_object()obj.title=titleself.objview.update_item(obj.uuid)
def__load_from_file(self,filename:str)->list[SignalObj]|list[ImageObj]:"""Open objects from file (signal/image), add them to DataLab and return them. Args: filename: file name Returns: New object or list of new objects """worker=CallbackWorker(lambdaworker:self.IO_REGISTRY.read(filename,worker))objs=qt_long_callback(self,_("Adding objects to workspace"),worker,True)group_id=Noneiflen(objs)>1:group_id=self.add_group(osp.basename(filename)).uuidforobjinobjs:obj.metadata["source"]=filenameself.add_object(obj,group_id=group_id,set_current=objisobjs[-1])self.selection_changed()returnobjsdef__save_to_file(self,obj:TypeObj,filename:str)->None:"""Save object to file (signal/image). Args: obj: object filename: file name """self.IO_REGISTRY.write(filename,obj)
[docs]defload_from_files(self,filenames:list[str]|None=None)->list[TypeObj]:"""Open objects from file (signals/images), add them to DataLab and return them. Args: filenames: File names Returns: list of new objects """ifnotself.mainwindow.confirm_memory_state():return[]iffilenamesisNone:# pragma: no coverbasedir=Conf.main.base_dir.get()filters=self.IO_REGISTRY.get_read_filters()withsave_restore_stds():filenames,_filt=getopenfilenames(self,_("Open"),basedir,filters)objs=[]forfilenameinfilenames:withqt_try_loadsave_file(self.parent(),filename,"load"):Conf.main.base_dir.set(filename)objs+=self.__load_from_file(filename)returnobjs
[docs]defsave_to_files(self,filenames:list[str]|str|None=None)->None:"""Save selected objects to files (signal/image). Args: filenames: File names """objs=self.objview.get_sel_objects(include_groups=True)iffilenamesisNone:# pragma: no coverfilenames=[None]*len(objs)assertlen(filenames)==len(objs),"Number of filenames must match number of objects"forindex,objinenumerate(objs):filename=filenames[index]iffilenameisNone:basedir=Conf.main.base_dir.get()filters=self.IO_REGISTRY.get_write_filters()withsave_restore_stds():filename,_filt=getsavefilename(self,_("Save as"),basedir,filters)iffilename:withqt_try_loadsave_file(self.parent(),filename,"save"):Conf.main.base_dir.set(filename)self.__save_to_file(obj,filename)
[docs]defexec_import_wizard(self)->None:"""Execute import wizard"""wizard=TextImportWizard(self.PANEL_STR_ID,parent=self.parent())ifexec_dialog(wizard):objs=wizard.get_objs()ifobjs:withcreate_progress_bar(self,_("Adding objects to workspace"),max_=len(objs)-1)asprogress:foridx,objinenumerate(objs):progress.setValue(idx)ifprogress.wasCanceled():breakself.add_object(obj)
[docs]defimport_metadata_from_file(self,filename:str|None=None)->None:"""Import metadata from file (JSON). Args: filename: File name """iffilenameisNone:# pragma: no coverbasedir=Conf.main.base_dir.get()withsave_restore_stds():filename,_filter=getopenfilename(self,_("Import metadata"),basedir,"*.json")iffilename:withqt_try_loadsave_file(self.parent(),filename,"load"):Conf.main.base_dir.set(filename)obj=self.objview.get_sel_objects(include_groups=True)[0]# Import object's metadata from file as JSON:handler=JSONHandler(filename)handler.load()obj.metadata=handler.get_json_dict()self.SIG_REFRESH_PLOT.emit("selected",True)
[docs]defexport_metadata_from_file(self,filename:str|None=None)->None:"""Export metadata to file (JSON). Args: filename: File name """obj=self.objview.get_sel_objects(include_groups=True)[0]iffilenameisNone:# pragma: no coverbasedir=Conf.main.base_dir.get()withsave_restore_stds():filename,_filt=getsavefilename(self,_("Export metadata"),basedir,"*.json")iffilename:withqt_try_loadsave_file(self.parent(),filename,"save"):Conf.main.base_dir.set(filename)# Export object's metadata to file as JSON:handler=JSONHandler(filename)handler.set_json_dict(obj.metadata)handler.save()
[docs]defproperties_changed(self)->None:"""The properties 'Apply' button was clicked: update object properties, refresh plot and update object view."""obj=self.objview.get_current_object()# if obj is not None: # XXX: Is it necessary?obj.invalidate_maskdata_cache()update_dataset(obj,self.objprop.properties.dataset)self.objview.update_item(obj.uuid)self.SIG_REFRESH_PLOT.emit("selected",True)
# ------Plotting data in modal dialogs----------------------------------------------
[docs]defopen_separate_view(self,oids:list[str]|None=None,edit_annotations:bool=False)->PlotDialog|None:""" Open separate view for visualizing selected objects Args: oids: Object IDs (default: None) edit_annotations: Edit annotations (default: False) Returns: Instance of PlotDialog """ifoidsisNone:oids=self.objview.get_sel_object_uuids(include_groups=True)obj=self.objmodel[oids[0]]# Create a new dialog and add plot items to itdlg=self.create_new_dialog(title=obj.titleiflen(oids)==1elseNone,edit=True,name=f"{obj.PREFIX}_new_window",options={"show_itemlist":edit_annotations},)ifdlgisNone:returnNoneself.add_plot_items_to_dialog(dlg,oids)mgr=dlg.get_manager()toolbar=QW.QToolBar(_("Annotations"),self)dlg.button_layout.insertWidget(0,toolbar)mgr.add_toolbar(toolbar,id(toolbar))toolbar.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon)fortoolinself.ANNOTATION_TOOLS:mgr.add_tool(tool,toolbar_id=id(toolbar))deftoggle_annotations(enabled:bool):"""Toggle annotation tools / edit buttons visibility"""forwidgetin(dlg.button_box,toolbar,mgr.get_itemlist_panel()):ifenabled:widget.show()else:widget.hide()edit_ann_action=create_action(dlg,_("Annotations"),toggled=toggle_annotations,icon=get_icon("annotations.svg"),)mgr.add_tool(ActionTool,edit_ann_action)default_toolbar=mgr.get_default_toolbar()action_btn=default_toolbar.widgetForAction(edit_ann_action)action_btn.setToolButtonStyle(QC.Qt.ToolButtonTextBesideIcon)plot=dlg.get_plot()foriteminplot.items:item.set_selectable(False)foriteminobj.iterate_shape_items(editable=True):plot.add_item(item)self.__separate_views[dlg]=objtoggle_annotations(edit_annotations)ifedit_annotations:edit_ann_action.setChecked(True)dlg.show()dlg.finished.connect(self.__separate_view_finished)returndlg
def__separate_view_finished(self,result:int)->None:"""Separate view was closed Args: result: result """dlg:PlotDialog=self.sender()ifresult==QW.QDialog.DialogCode.Accepted:rw_items=[]foritemindlg.get_plot().get_items():ifnotitem.is_readonly()andis_plot_item_serializable(item):rw_items.append(item)obj=self.__separate_views[dlg]obj.annotations=items_to_json(rw_items)self.selection_changed(update_items=True)self.__separate_views.pop(dlg)dlg.deleteLater()
[docs]defcreate_new_dialog(self,edit:bool=False,toolbar:bool=True,title:str|None=None,name:str|None=None,options:dict|None=None,)->PlotDialog|None:"""Create new pop-up signal/image plot dialog. Args: edit: Edit mode toolbar: Show toolbar title: Dialog title name: Dialog object name options: Plot options Returns: Plot dialog instance """plot_options=self.plothandler.get_current_plot_options()ifoptionsisnotNone:plot_options=plot_options.copy(options)# Resize the dialog so that it's at least MINDIALOGSIZE (absolute values),# and at most MAXDIALOGSIZE (% of the main window size):minwidth,minheight=self.MINDIALOGSIZEmaxwidth=int(self.mainwindow.width()*self.MAXDIALOGSIZE)maxheight=int(self.mainwindow.height()*self.MAXDIALOGSIZE)size=min(minwidth,maxwidth),min(minheight,maxheight)# pylint: disable=not-callabledlg=PlotDialog(parent=self.parent(),title=APP_NAMEiftitleisNoneelsef"{title} - {APP_NAME}",edit=edit,options=plot_options,toolbar=toolbar,size=size,)dlg.setWindowIcon(get_icon("DataLab.svg"))dlg.setObjectName(name)returndlg
[docs]defget_roi_editor_output(self,extract:bool)->tuple[TypeROI,bool]|None:"""Get ROI data (array) from specific dialog box. Args: extract: Extract ROI from data Returns: A tuple containing the ROI object and a boolean indicating whether the dialog was accepted or not. """roi_s=_("Regions of interest")options=self.ROIDIALOGOPTIONSobj=self.objview.get_sel_objects(include_groups=True)[0]# Create a new dialogdlg=self.create_new_dialog(edit=True,toolbar=True,title=f"{roi_s} - {obj.title}",name=f"{obj.PREFIX}_roi_dialog",options=options,)ifdlgisNone:returnNone# Create ROI editor (and add it to the dialog)# pylint: disable=not-callableitem=obj.make_item(update_from=self.plothandler[obj.uuid])roi_editor=self.get_roieditor_class()(dlg,obj,extract,item=item)dlg.button_layout.insertWidget(0,roi_editor)ifexec_dialog(dlg):returnroi_editor.get_roieditor_results()returnNone
[docs]defget_objects_with_dialog(self,title:str,comment:str="",nb_objects:int=1,parent:QW.QWidget|None=None,)->TypeObj|None:"""Get object with dialog box. Args: title: Dialog title comment: Optional dialog comment nb_objects: Number of objects to select parent: Parent widget Returns: Object(s) (signal(s) or image(s), or None if dialog was canceled) """parent=selfifparentisNoneelseparentdlg=objectview.GetObjectsDialog(parent,self,title,comment,nb_objects)ifexec_dialog(dlg):returndlg.get_selected_objects()returnNone
def__new_objprop_button(self,title:str,icon:str,tooltip:str,callback:Callable)->QW.QPushButton:"""Create new object property button"""btn=QW.QPushButton(get_icon(icon),title,self)btn.setToolTip(tooltip)self.objprop.add_button(btn)btn.clicked.connect(callback)self.acthandler.add_action(btn,select_condition=actionhandler.SelectCond.at_least_one,)returnbtn
[docs]defadd_objprop_buttons(self)->None:"""Insert additional buttons in object properties panel"""self.__new_objprop_button(_("Results"),"show_results.svg",_("Show results obtained from previous analysis"),self.show_results,)self.__new_objprop_button(_("Annotations"),"annotations.svg",_("Open a dialog to edit annotations"),lambda:self.open_separate_view(edit_annotations=True),)
def__show_no_result_warning(self):"""Show no result warning"""msg="<br>".join([_("No result currently available for this object."),"",_("This feature leverages the results of previous analysis ""performed on the selected object(s).<br><br>""To compute results, select one or more objects and choose ""a feature in the <u>Analysis</u> menu."),])QW.QMessageBox.information(self,APP_NAME,msg)
def__add_result_signal(self,x:np.ndarray|list[float],y:np.ndarray|list[float],title:str,xaxis:str,yaxis:str,)->None:"""Add result signal"""xdata=np.array(x,dtype=float)ydata=np.array(y,dtype=float)obj=create_signal(title=f"{title}: {yaxis} = f({xaxis})",x=xdata,y=ydata,labels=[xaxis,yaxis],)self.mainwindow.signalpanel.add_object(obj)
[docs]defplot_results(self)->None:"""Plot results"""objs=self.objview.get_sel_objects(include_groups=True)rdatadict=create_resultdata_dict(objs)ifrdatadict:forcategory,rdatainrdatadict.items():xchoices=(("indices",_("Indices")),)forxlabelinrdata.xlabels:xchoices+=((xlabel,xlabel),)ychoices=xchoices[1:]# Regrouping ResultShape results by their `title` attribute:grouped_results:dict[str,list[ResultShape]]={}forresultinrdata.results:grouped_results.setdefault(result.title,[]).append(result)# From here, results are already grouped by their `category` attribute,# and then by their `title` attribute. We can now plot them.## Now, we have two common use cases:# 1. Plotting one curve per object (signal/image) and per `title`# attribute, if each selected object contains a result object# with multiple values (e.g. from a blob detection feature).# 2. Plotting one curve per `title` attribute, if each selected object# contains a result object with a single value (e.g. from a FHWM# feature) - in that case, we select only the first value of each# result object.# The default kind of plot depends on the number of values in each# result and the number of selected objects:default_kind=("one_curve_per_object"ifany(result.array.shape[0]>1forresultinrdata.results)else"one_curve_per_title")classPlotResultParam(gds.DataSet):"""Plot results parameters"""kind=gds.ChoiceItem(_("Plot kind"),(("one_curve_per_object",_("One curve per object (or ROI) and per result title"),),("one_curve_per_title",_("One curve per result title")),),default=default_kind,)xaxis=gds.ChoiceItem(_("X axis"),xchoices,default="indices")yaxis=gds.ChoiceItem(_("Y axis"),ychoices,default=ychoices[0][0])comment=(_("Plot results obtained from previous analyses.<br><br>""This plot is based on results associated with '%s' prefix.")%category)param=PlotResultParam(_("Plot results"),comment=comment)ifnotparam.edit(parent=self.parent()):returni_yaxis=rdata.xlabels.index(param.yaxis)ifparam.kind=="one_curve_per_title":# One curve per result title:fortitle,resultsingrouped_results.items():# titlex,y=[],[]forindex,resultinenumerate(results):# objectifparam.xaxis=="indices":x.append(index)else:i_xaxis=rdata.xlabels.index(param.xaxis)x.append(result.shown_array[0][i_xaxis])y.append(result.shown_array[0][i_yaxis])self.__add_result_signal(x,y,title,param.xaxis,param.yaxis)else:# One curve per result title, per object and per ROI:fortitle,resultsingrouped_results.items():# titleforindex,resultinenumerate(results):# objectroi_idx=np.array(np.unique(result.array[:,0]),dtype=int)fori_roiinroi_idx:# ROImask=result.array[:,0]==i_roiifparam.xaxis=="indices":x=np.arange(result.array.shape[0])[mask]else:i_xaxis=rdata.xlabels.index(param.xaxis)x=result.shown_array[mask,i_xaxis]y=result.shown_array[mask,i_yaxis]stitle=f"{title} ({objs[index].short_id})"iflen(roi_idx)>1:stitle+=f"|ROI{i_roi}"self.__add_result_signal(x,y,stitle,param.xaxis,param.yaxis)else:self.__show_no_result_warning()
[docs]defdelete_results(self)->None:"""Delete results"""objs=self.objview.get_sel_objects(include_groups=True)rdatadict=create_resultdata_dict(objs)ifrdatadict:answer=QW.QMessageBox.warning(self,_("Delete results"),_("Are you sure you want to delete all results ""of the selected object(s)?"),QW.QMessageBox.Yes|QW.QMessageBox.No,)ifanswer==QW.QMessageBox.Yes:objs=self.objview.get_sel_objects(include_groups=True)forobjinobjs:obj.delete_results()self.SIG_REFRESH_PLOT.emit("selected",True)else:self.__show_no_result_warning()
[docs]defadd_label_with_title(self,title:str|None=None)->None:"""Add a label with object title on the associated plot Args: title: Label title. Defaults to None. If None, the title is the object title. """objs=self.objview.get_sel_objects(include_groups=True)forobjinobjs:obj.add_label_with_title(title=title)self.SIG_REFRESH_PLOT.emit("selected",True)