# 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__importannotationsimportabcimportdataclassesimportglobimportos.pathasospimportreimportwarningsfromtypingimportTYPE_CHECKING,Generic,Typeimportguidata.datasetasgdsimportguidata.dataset.qtwidgetsasgdqimportnumpyasnpimportplotpy.iofromguidata.configtoolsimportget_iconfromguidata.datasetimportupdate_datasetfromguidata.ioimportJSONHandlerfromguidata.qthelpersimportadd_actions,create_action,exec_dialogfromguidata.widgets.arrayeditorimportArrayEditorfromplotpy.plotimportPlotDialogfromplotpy.toolsimportActionToolfromqtpyimportQtCoreasQCfromqtpyimportQtWidgetsasQWfromqtpy.compatimport(getexistingdirectory,getopenfilename,getopenfilenames,getsavefilename,)fromcdl.configimportAPP_NAME,Conf,_fromcdl.core.guiimportactionhandler,objectmodel,objectviewfromcdl.core.gui.roieditorimportTypeROIEditorfromcdl.core.model.baseimport(ANN_KEY,ROI_KEY,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,MaskedImageItemfromcdl.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]classPasteMetadataParam(gds.DataSet):"""Paste metadata parameters"""keep_roi=gds.BoolItem(_("Regions of interest"),default=True)keep_resultshapes=gds.BoolItem(_("Result shapes"),default=False).set_pos(col=1)keep_annotations=gds.BoolItem(_("Annotations"),default=True)keep_resultproperties=gds.BoolItem(_("Result properties"),default=False).set_pos(col=1)keep_other=gds.BoolItem(_("Other metadata"),default=True)
[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,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:# Ensure that item's parameters are up-to-date:item.param.update_param(item)# Update object metadata from plot item parametersobj.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.refresh_plot("selected",True,False)super().remove_all_objects()
# ---- Signal/Image Panel API ------------------------------------------------------
[docs]defrefresh_plot(self,what:str,update_items:bool=True,force:bool=False)->None:"""Refresh plot. This method simply emits the signal SIG_REFRESH_PLOT which is connected to the method `PlotHandler.refresh_plot`. Args: what: string describing the objects to refresh. Valid values are "selected" (refresh the selected objects), "all" (refresh all objects), "existing" (refresh existing plot items), or an object uuid. update_items: if True, update the items. If False, only show the items (do not update them, except if the option "Use reference item LUT range" is enabled and more than one item is selected). Defaults to True. force: if True, force refresh even if auto refresh is disabled, and refresh all items associated to objects (even the hidden ones, e.g. when selecting multiple images of the same size and position). Defaults to False. Raises: ValueError: if `what` is not a valid value """ifwhatnotin("selected","all","existing")andnotisinstance(what,str):raiseValueError(f"Invalid value for 'what': {what}")self.SIG_REFRESH_PLOT.emit(what,update_items,force)
[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]defadd_group(self,title:str,select:bool=False)->objectmodel.ObjectGroup:"""Add group Args: title: group title select: if True, select the group in the tree view. Defaults to False. Returns: Created group object """group=self.objmodel.add_group(title)self.objview.add_group_item(group)ifselect:self.objview.select_groups([group])returngroup
[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,param:PasteMetadataParam|None=None)->None:"""Paste metadata to selected object(s)"""ifparamisNone:param=PasteMetadataParam(_("Paste metadata"),comment=_("Select what to keep from the clipboard.<br><br>""Result shapes and annotations, if kept, will be merged with ""existing ones. <u>All other metadata will be replaced</u>."),)ifnotparam.edit(parent=self.parent()):returnmetadata={}ifparam.keep_roiandROI_KEYinself.__metadata_clipboard:metadata[ROI_KEY]=self.__metadata_clipboard[ROI_KEY]ifparam.keep_annotationsandANN_KEYinself.__metadata_clipboard:metadata[ANN_KEY]=self.__metadata_clipboard[ANN_KEY]ifparam.keep_resultshapes:forkey,valueinself.__metadata_clipboard.items():ifResultShape.match(key,value):metadata[key]=valueifparam.keep_resultproperties:forkey,valueinself.__metadata_clipboard.items():ifResultProperties.match(key,value):metadata[key]=valueifparam.keep_other:forkey,valueinself.__metadata_clipboard.items():if(notResultShape.match(key,value)andnotResultProperties.match(key,value)andkeynotin(ROI_KEY,ANN_KEY)):metadata[key]=valuesel_objects=self.objview.get_sel_objects(include_groups=True)forobjinsorted(sel_objects,key=lambdaobj:obj.short_id,reverse=True):obj.update_metadata_from(metadata)# We have to do a manual refresh in order to force the plot handler to update# all plot items, even the ones that are not visible (otherwise, image masks# would not be updated after pasting the metadata: see issue #123)self.manual_refresh()
[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:# We have to do a manual refresh in order to force the plot handler to# update all plot items, even the ones that are not visible (otherwise,# image masks would remained visible after deleting the ROI for example:# see issue #122)self.manual_refresh()
[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_selected_object_or_group(self,new_name:str|None=None)->None:"""Rename selected object or group Args: new_name: new name (default: None, i.e. ask user) """sel_objects=self.objview.get_sel_objects(include_groups=False)sel_groups=self.objview.get_sel_groups()if(notsel_objectsandnotsel_groups)orlen(sel_objects)+len(sel_groups)>1:# Won't happen in the application, but could happen in tests or using the# API directlyraiseValueError("Select one object or group to rename")ifsel_objects:obj=sel_objects[0]ifnew_nameisNone:new_name,ok=QW.QInputDialog.getText(self,_("Rename object"),_("Object name:"),QW.QLineEdit.Normal,obj.title,)ifnotok:returnobj.title=new_nameself.objview.update_item(obj.uuid)self.objprop.update_properties_from(obj)elifsel_groups: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_directory(self,directory:str|None=None)->list[TypeObj]:"""Open objects from directory (signals or images, depending on the panel), add them to DataLab and return them. If the directory is not specified, ask the user to select a directory. Args: directory: directory name Returns: list of new objects """ifnotself.mainwindow.confirm_memory_state():return[]ifdirectoryisNone:# pragma: no coverbasedir=Conf.main.base_dir.get()withsave_restore_stds():directory=getexistingdirectory(self,_("Open"),basedir)ifnotdirectory:return[]# Get all files in the directory:relfnames=sorted(osp.relpath(path,start=directory)forpathinglob.glob(osp.join(directory,"**","*.*"),recursive=True))# When Python 3.9 will be dropped, we can use (support for `root_dir` is added):# relfnames = sorted(glob.glob("**/*.*", root_dir=directory, recursive=True))filenames=[osp.join(directory,fname)forfnameinrelfnames]returnself.load_from_files(filenames,ignore_unknown=True)
[docs]defload_from_files(self,filenames:list[str]|None=None,ignore_unknown:bool=False)->list[TypeObj]:"""Open objects from file (signals/images), add them to DataLab and return them. Args: filenames: File names ignore_unknown: if True, ignore unknown file types (default: False) 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)try:objs+=self.__load_from_file(filename)exceptNotImplementedErrorasexc:ifignore_unknown:# Ignore unknown file typespasselse:raiseexcreturnobjs
[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.refresh_plot("selected",True,False)
[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.refresh_plot("selected",True,False)
# ------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)ifobj.uuidnotinself.plothandler:# This happens for example when opening an already saved workspace with# multiple images, and if the user tries to edit the ROI of a group of# images without having selected any object yet. In this case, only the# last image is actually plotted (because if the other have the same size# and position, they are hidden), and the plot item of the first image is# not created yet. The `obj.uuid` is precisely the uuid of the first image.self.plothandler.refresh_plot("selected",True,True)item=obj.make_item(update_from=self.plothandler[obj.uuid])# pylint: disable=not-callableroi_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 ROI (if any) and per result title# ------------------------------------------------------------------# Begin by checking if all results have the same number of ROIs:# for simplicity, let's check the number of unique ROI indices.all_roi_indexes=[np.unique(result.array[:,0])forresultinrdata.results]# Check if all roi_indexes are the same:iflen(set(map(tuple,all_roi_indexes)))>1:QW.QMessageBox.warning(self,_("Plot results"),_("All objects associated with results must have the ""same regions of interest (ROIs) to plot results ""together."),)returnfori_roiinall_roi_indexes[0]:roi_suffix=f"|ROI{int(i_roi+1)}"ifi_roi>=0else""fortitle,resultsingrouped_results.items():# titlex,y=[],[]forindex,resultinenumerate(results):mask=result.array[:,0]==i_roiifparam.xaxis=="indices":x.append(index)else:i_xaxis=rdata.xlabels.index(param.xaxis)x.append(result.shown_array[mask,i_xaxis][0])y.append(result.shown_array[mask,i_yaxis][0])self.__add_result_signal(x,y,f"{title}{roi_suffix}",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:# ROIroi_suffix=f"|ROI{int(i_roi+1)}"ifi_roi>=0else""mask=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}){roi_suffix}"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.refresh_plot("selected",True,False)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.refresh_plot("selected",True,False)