From 0d7815be2f502cd095f38d301829480f82928976 Mon Sep 17 00:00:00 2001 From: "nicholas.anthony@northwestern.edu" Date: Fri, 15 Oct 2021 16:49:07 -0500 Subject: [PATCH 1/8] accomodate change in mpl_qt_viz api --- conda.recipe/meta.yaml | 2 +- setup.py | 2 +- src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index dfb9bef..8dccdcb 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -33,7 +33,7 @@ requirements: - google-auth-oauthlib - pyqt =5 - pwspy >=0.2.8 # Core pws package, available on backmanlab anaconda cloud account. - - mpl_qt_viz >=1.0.9 # Plotting package available on PyPi and the backmanlab anaconda cloud account and conda-forge. Written for this project by Nick Anthony + - mpl_qt_viz >1.0.9 # Plotting package available on PyPi and the backmanlab anaconda cloud account and conda-forge. Written for this project by Nick Anthony - descartes - cachetools >=4 app: diff --git a/setup.py b/setup.py index d25a2bf..aaa89f3 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ 'google-auth-oauthlib', 'PyQt5', 'pwspy>=0.2.8', # Core pws package, available on backmanlab anaconda cloud account. - 'mpl_qt_viz>=1.0.9', # Plotting package available on PyPi and the backmanlab anaconda cloud account. Written for this project by Nick Anthony + 'mpl_qt_viz>1.0.9', # Plotting package available on PyPi and the backmanlab anaconda cloud account. Written for this project by Nick Anthony 'descartes', 'cachetools>=4'], package_dir={'': 'src'}, diff --git a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py index 10b3cc3..911f3de 100644 --- a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py +++ b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py @@ -345,7 +345,6 @@ def _moveRoisFunc(self): def done(vertsSet, handles): self._polyWidg.set_active(False) - self._polyWidg.set_visible(False) for param, verts in zip(selectedROIParams, vertsSet): newRoi = pwsdt.Roi.fromVerts(np.array(verts), param.roiFile.getRoi().mask.shape) @@ -380,7 +379,6 @@ def done(verts, handles): verts = verts[0] newRoi = pwsdt.Roi.fromVerts(np.array(verts), selectedROIParam.roiFile.getRoi().mask.shape) self._polyWidg.set_active(False) - self._polyWidg.set_visible(False) self._roiManager.updateRoi(selectedROIParam.roiFile, newRoi) self.roiModified.emit(self.metadata, selectedROIParam.roiFile) From a9657ec034653abd3d99c3adacb1277d05cf899a Mon Sep 17 00:00:00 2001 From: Nick Anthony Date: Thu, 21 Oct 2021 14:30:46 -0500 Subject: [PATCH 2/8] add correct names to plotND in ERWorkflow --- src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py index a2e084a..6279149 100644 --- a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py +++ b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py @@ -199,16 +199,19 @@ def save(self, includeSettings: t_.List[str], binning: int, parallelProcessing: erCube, rExtraDict = er.generateRExtraCubes(combos, theoryR, numericalAperture) dock = DockablePlotWindow(title=setting) dock.addWidget( - PlotNd(erCube.data, title='Mean', + PlotNd(erCube.data, + title='Mean', indices=[range(erCube.data.shape[0]), range(erCube.data.shape[1]), - erCube.wavelengths]), + erCube.wavelengths], + names=('y', 'x', 'lambda')), title='Mean' ) for matCombo, rExtraArr in rExtraDict.items(): dock.addWidget( PlotNd(rExtraArr, title=matCombo, indices=[range(erCube.data.shape[0]), range(erCube.data.shape[1]), - erCube.wavelengths]), + erCube.wavelengths], + names=('y', 'x', 'lambda')), title=str(matCombo) ) logger = logging.getLogger(__name__) From 1c10bf0900b584c6b08d6c030d112c36eed99fc3 Mon Sep 17 00:00:00 2001 From: Nick Anthony Date: Thu, 21 Oct 2021 15:07:03 -0500 Subject: [PATCH 3/8] a little commenting and type hinting for ERWorkflow --- .../ExtraReflectanceCreator/ERWorkFlow.py | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py index 6279149..0a17147 100644 --- a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py +++ b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU General Public License # along with PWSpy. If not, see . - +import dataclasses import hashlib import os from datetime import datetime @@ -39,11 +39,17 @@ from mpl_qt_viz.visualizers import PlotNd, DockablePlotWindow import pathlib as pl +DirectoryDataFrame = t_.NewType("DirectoryDataFrame", pd.DataFrame) # Type alias representing the data frame returned by `scanDirectory` +# Has columns "setting", "cube", "material" + +@dataclasses.dataclass class Directory: - def __init__(self, df: pd.DataFrame, camCorr: CameraCorrection): - self.dataframe = df - self.cameraCorrection = camCorr + """ + Groups together relevant data for a given "System Directory" in the ERCreator raw data collection. + """ + dataframe: DirectoryDataFrame + cameraCorrection: CameraCorrection def scanDirectory(directory: str) -> Directory: @@ -75,20 +81,26 @@ def scanDirectory(directory: str) -> Directory: m = matMap[filelist[-2]] file = Acquisition(file).pws.filePath # old pws is saved directly in the "Cell{X}" folder. new pws is saved in "Cell{x}/PWS" the Acquisition class helps us abstract that out and be compatible with both. rows.append({'setting': s, 'material': m, 'cube': file}) - df = pd.DataFrame(rows) - return Directory(df, cam) + df: DirectoryDataFrame = pd.DataFrame(rows) + return Directory(dataframe=df, cameraCorrection=cam) class DataProvider: - def __init__(self, df: pd.DataFrame, camCorr: CameraCorrection): - self._df = df - self._cameraCorrection = camCorr - self._cubes = None + """ + This object manages caching data and processing new data when it needs to be loaded. + + Args: + directory: The Directory object providing a reference to the actual data. + """ + def __init__(self, directory: Directory): + self._df = directory.dataframe + self._cameraCorrection = directory.cameraCorrection + self._cubes: pd.DataFrame = None - def getCubes(self): + def getCubes(self) -> t_.Optional[pd.DataFrame]: return self._cubes - def getDataFrame(self): + def getDataFrame(self) -> pd.DataFrame: return self._df def loadCubes(self, includeSettings: t_.List[str], binning: int, parallelProcessing: bool): From 10e21172d68f5547fd71498b00fa21185c9eb1ef Mon Sep 17 00:00:00 2001 From: Nick Anthony Date: Fri, 22 Oct 2021 09:02:59 -0500 Subject: [PATCH 4/8] ... --- src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py index 911f3de..d63d280 100644 --- a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py +++ b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py @@ -186,7 +186,7 @@ def update_annot(roiFile: pwsdt.RoiFile, poly: PathPatch): self.annot.xy = poly.get_path().vertices.mean(axis=0) # Set the location to the center of the polygon. text = f"{roiFile.name}, {roiFile.number}" if self.metadata.pws: # A day may come where fluorescence is not taken on the same camera as pws, in this case we will have multiple pixel sizes and ROI handling will need an update. for now just assume we'll use PWS pixel size - if self.metadata.pws.pixelSizeUm: # For some systems (nanocytomics) this is None + if self.metadata.pws.pixelSizeUm: # For some systems (NC) this is None text += f"\n{self.metadata.pws.pixelSizeUm ** 2 * np.sum(roiFile.getRoi().mask):.2f} $μm^2$" self.annot.set_text(text) self.annot.get_bbox_patch().set_alpha(0.4) From 1d36288ecb5ddb4229c7fcfbf057d35c5f63ad3e Mon Sep 17 00:00:00 2001 From: Nick Anthony Date: Thu, 18 Nov 2021 13:57:09 -0600 Subject: [PATCH 5/8] refactoring the extra reflection data directory functionality --- .../ExtraReflectanceCreator/ERWorkFlow.py | 2 +- .../_dockWidgets/ResultsTableDock/widgets.py | 2 ++ .../ERDataComparator.py | 14 +++++------ .../_ERDataDirectory.py | 10 ++++++++ .../_ERSelectorWindow.py | 8 +++---- .../_ERUploaderWindow.py | 19 ++++++++------- .../extraReflectionManager/__init__.py | 24 +++++++++++++------ 7 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py index 0a17147..d5c0dd9 100644 --- a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py +++ b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py @@ -299,5 +299,5 @@ def directoryChanged(self, directory: str) -> t_.Set[str]: """ self.currDir = directory directory = self.fileStruct[directory] - self.dataprovider = DataProvider(directory.dataframe, directory.cameraCorrection) + self.dataprovider = DataProvider(directory) return set(self.dataprovider.getDataFrame()['setting']) diff --git a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/widgets.py b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/widgets.py index 6715aea..baa3079 100644 --- a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/widgets.py +++ b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/widgets.py @@ -83,6 +83,8 @@ def _plotOpd(self): fig, ax = plt.subplots() ax.plot(self.results.pws.opdIndex, self.results.pws.opd) fig.suptitle(f"{self.cellPathLabel.text()}/Cell{self.cellNumLabel.text()}") + ax.set_ylabel("Amplitude") + ax.set_xlabel("OPD (um)") fig.show() diff --git a/src/pwspy_gui/sharedWidgets/extraReflectionManager/ERDataComparator.py b/src/pwspy_gui/sharedWidgets/extraReflectionManager/ERDataComparator.py index a3a4e70..270a117 100644 --- a/src/pwspy_gui/sharedWidgets/extraReflectionManager/ERDataComparator.py +++ b/src/pwspy_gui/sharedWidgets/extraReflectionManager/ERDataComparator.py @@ -22,16 +22,16 @@ import pandas import typing if typing.TYPE_CHECKING: - from pwspy_gui.sharedWidgets.extraReflectionManager import ERManager, ERDownloader -from pwspy_gui.sharedWidgets.extraReflectionManager._ERDataDirectory import ERDataDirectory, EROnlineDirectory + from pwspy_gui.sharedWidgets.extraReflectionManager import ERDownloader +from pwspy_gui.sharedWidgets.extraReflectionManager._ERDataDirectory import ERDataDirectory, EROnlineDirectory, ERAbstractDirectory from enum import Enum class ERDataComparator: """A class to compare the local directory to the online directory. Args: - downloader (Optional[ERDownloader]): Handles communication with GoogleDrive, if operating in offline mode this should be None - directory (str): The file path where the files are stored locally. + online: Handles communication with GoogleDrive, if operating in offline mode this should be None + directory: The file path where the files are stored locally. """ class ComparisonStatus(Enum): @@ -40,9 +40,9 @@ class ComparisonStatus(Enum): Md5Mismatch = 'MD5 Mismatch' Match = "Match" # This is what we hope to see. - def __init__(self, downloader: Optional[ERDownloader], directory: str): - self.local: ERDataDirectory = ERDataDirectory(directory) - self.online: Optional[EROnlineDirectory] = None if downloader is None else EROnlineDirectory(downloader) + def __init__(self, online: EROnlineDirectory, directory: ERDataDirectory): + self.local: ERDataDirectory = directory + self.online: EROnlineDirectory = online def updateIndexes(self): self.local.updateIndex() diff --git a/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERDataDirectory.py b/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERDataDirectory.py index c09bf96..29747a2 100644 --- a/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERDataDirectory.py +++ b/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERDataDirectory.py @@ -66,6 +66,15 @@ def __init__(self, directory: str): self._directory = directory super().__init__() + def getMetadataFromId(self, idTag: str) -> ERMetaData: + """Given the unique idTag string for an ExtraReflectanceCube this will search the index.json and return the + ERMetaData file. If it cannot be found then an `IndexError will be raised.""" + try: + match = [item for item in self.index.cubes if item.idTag == idTag][0] + except IndexError: + raise IndexError(f"An ExtraReflectanceCube with idTag {idTag} was not found in the index.json file at {self._directory}.") + return ERMetaData.fromHdfFile(self._directory, match.name) + def updateIndex(self): indexPath = os.path.join(self._directory, ERIndex.FILENAME) if not os.path.exists(indexPath): @@ -213,4 +222,5 @@ def _compareIndexes(calculatedIndex: ERIndex, jsonIndex: ERIndex) -> dict: else: raise Exception("Programming error.") # This shouldn't be possible d[i] = {'idTag': [ind.idTag for ind in jsonIndex.cubes if ind.fileName == fileName][0], 'status': status} + return d diff --git a/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERSelectorWindow.py b/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERSelectorWindow.py index 5860d83..849e892 100644 --- a/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERSelectorWindow.py +++ b/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERSelectorWindow.py @@ -110,9 +110,9 @@ def __init__(self, manager: ERManager, parent: Optional[QWidget] = None): def _initialize(self): self._items: List[ERTreeWidgetItem] = [] self.tree.clear() - self._manager.dataComparator.local.updateIndex() - self.fileStatus = self._manager.dataComparator.local.getFileStatus(skipMD5=True) # Skipping the md5 hash check should speed things up here. - for item in self._manager.dataComparator.local.index.cubes: + self._manager.localDirectory.updateIndex() + self.fileStatus = self._manager.localDirectory.getFileStatus(skipMD5=True) # Skipping the md5 hash check should speed things up here. + for item in self._manager.localDirectory.index.cubes: self._addItem(item) # Sort items by date for item in [self.tree.invisibleRootItem().child(i) for i in range(self.tree.invisibleRootItem().childCount())]: @@ -123,7 +123,7 @@ def _initialize(self): def _addItem(self, item: ERIndexCube): treeItem = ERTreeWidgetItem(fileName=item.fileName, description=item.description, idTag=item.idTag, name=item.name, - downloaded=self.fileStatus[self.fileStatus['idTag'] == item.idTag].iloc[0]['Local Status'] == self._manager.dataComparator.local.DataStatus.found.value) + downloaded=self.fileStatus[self.fileStatus['idTag'] == item.idTag].iloc[0]['Local Status'] == self._manager.localDirectory.DataStatus.found.value) self._items.append(treeItem) _ = self.tree.invisibleRootItem() if treeItem.configurationName not in [_.child(i).text(0) for i in range(_.childCount())]: diff --git a/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERUploaderWindow.py b/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERUploaderWindow.py index 67d2839..c4a3fb5 100644 --- a/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERUploaderWindow.py +++ b/src/pwspy_gui/sharedWidgets/extraReflectionManager/_ERUploaderWindow.py @@ -45,6 +45,7 @@ class ERUploaderWindow(QDialog): """This window provides the user a visual picture of the local and online status of the Extra Reflectance calibration file repository. It also allows uploading of files that are present locally but not on the server. It does not have good handling of edge cases, e.g. online server in inconsistent state.""" def __init__(self, manager: ERManager, parent: Optional[QWidget] = None): + self._dataComparator = ERDataComparator(manager.onlineDirectory, manager.localDirectory) self._manager = manager self._selectedId: str = None super().__init__(parent) @@ -52,7 +53,7 @@ def __init__(self, manager: ERManager, parent: Optional[QWidget] = None): self.setWindowTitle("Extra Reflectance File Manager") self.setLayout(QVBoxLayout()) self.table = QTableView(self) - self.fileStatus = self._manager.dataComparator.compare() + self.fileStatus = self._dataComparator.compare() self.table.setModel(PandasModel(self.fileStatus)) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setSelectionBehavior(QTableView.SelectRows) @@ -84,7 +85,7 @@ def displayInfo(self, index: QModelIndex): def plotData(self, index: QModelIndex): idTag = self.fileStatus.iloc[index.row()]['idTag'] - md = self._manager.getMetadataFromId(idTag) + md = self._dataComparator.local.getMetadataFromId(idTag) erCube = ExtraReflectanceCube.fromMetadata(md) self.plotHandle = PlotNd(erCube.data) @@ -96,7 +97,7 @@ def openContextMenu(self, pos: QPoint): displayAction = QAction("Display Info") displayAction.triggered.connect(lambda: self.displayInfo(index)) menu.addAction(displayAction) - if row['Local Status'] == self._manager.dataComparator.local.DataStatus.found.value: + if row['Local Status'] == self._dataComparator.local.DataStatus.found.value: plotAction = QAction("Plot Local Data") plotAction.triggered.connect(lambda: self.plotData(index)) menu.addAction(plotAction) @@ -111,7 +112,7 @@ def _updateGDrive(self): uploadableRows = (status['Index Comparison'] == ERDataComparator.ComparisonStatus.LocalOnly.value) | (status['Online Status'] == ERDataDirectory.DataStatus.missing.value) if np.any(uploadableRows): # There is something to upload for i, row, in status.loc[uploadableRows].iterrows(): - fileName = [i.fileName for i in self._manager.dataComparator.local.index.cubes if i.idTag == row['idTag']][0] + fileName = [i.fileName for i in self._dataComparator.local.index.cubes if i.idTag == row['idTag']][0] self._manager.upload(fileName) self._manager.upload('index.json') self.refresh() @@ -122,14 +123,14 @@ def _updateGDrive(self): def refresh(self): """Scans local and online files to refresh the display.""" - self._manager.dataComparator.updateIndexes() - self.fileStatus = self._manager.dataComparator.compare() + self._dataComparator.updateIndexes() + self.fileStatus = self._dataComparator.compare() self.table.setModel(PandasModel(self.fileStatus)) def _updateIndexFile(self): - self._manager.dataComparator.online.updateIndex() - index = ERIndex.merge(self._manager.dataComparator.local.index, self._manager.dataComparator.online.index) - self._manager.dataComparator.local.saveNewIndex(index) + self._dataComparator.online.updateIndex() + index = ERIndex.merge(self._dataComparator.local.index, self._dataComparator.online.index) + self._dataComparator.local.saveNewIndex(index) self.refresh() diff --git a/src/pwspy_gui/sharedWidgets/extraReflectionManager/__init__.py b/src/pwspy_gui/sharedWidgets/extraReflectionManager/__init__.py index 2ef4187..8110ba9 100644 --- a/src/pwspy_gui/sharedWidgets/extraReflectionManager/__init__.py +++ b/src/pwspy_gui/sharedWidgets/extraReflectionManager/__init__.py @@ -72,9 +72,23 @@ def __init__(self, filePath: str, parentWidget: QWidget = None): indexPath = os.path.join(self._directory, 'index.json') if not os.path.exists(indexPath) and not self.offlineMode: self.download('index.json') - self.dataComparator = ERDataComparator(self._downloader, self._directory) + self.localDirectory = ERDataDirectory(self._directory) + if not self.offlineMode: + self.onlineDirectory = EROnlineDirectory(self._downloader) + else: + self.onlineDirectory = None + + def _logIn(self, parentWidget: Optional[QWidget]) -> typing.Tuple[bool, Optional[ERDownloader]]: + """ + Try logging in to google drive + Args: + parentWidget: If a message box needs to be displayed this widget will act as the parent of the message box. - def _logIn(self, parentWidget: QWidget) -> typing.Tuple[bool, ERDownloader]: + Returns: + offlineMode: True if connection failed. + downloader: A download helper object. Will be none if connection failed. + + """ logger = logging.getLogger(__name__) logger.debug("Calling ERDownloader.getCredentials") creds = ERDownloader.getCredentials(applicationVars.googleDriveAuthPath) @@ -118,11 +132,7 @@ def upload(self, fileName: str): def getMetadataFromId(self, idTag: str) -> ERMetaData: """Given the unique idTag string for an ExtraReflectanceCube this will search the index.json and return the ERMetaData file. If it cannot be found then an `IndexError will be raised.""" - try: - match = [item for item in self.dataComparator.local.index.cubes if item.idTag == idTag][0] - except IndexError: - raise IndexError(f"An ExtraReflectanceCube with idTag {idTag} was not found in the index.json file at {self._directory}.") - return ERMetaData.fromHdfFile(self._directory, match.name) + return self.localDirectory.getMetadataFromId(idTag) class ERDownloader: From 34fa81be4f5e74779c290d8db816eec1bf49760f Mon Sep 17 00:00:00 2001 From: Nick Anthony Date: Wed, 1 Dec 2021 22:51:37 -0600 Subject: [PATCH 6/8] update class name in most recent PWSpy version --- src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py | 6 +++--- .../_dockWidgets/AnalysisSettingsDock/runtimeSettings.py | 8 ++++---- .../_dockWidgets/CellSelectorDock/widgets.py | 6 +++--- .../_dockWidgets/ResultsTableDock/__init__.py | 6 +++--- .../PWSAnalysisApp/_taskManagers/analysisManager.py | 6 +++--- src/pwspy_gui/PWSAnalysisApp/dialogs.py | 6 +++--- .../PWSAnalysisApp/sharedWidgets/plotting/_widgets.py | 2 +- src/pwspy_gui/PWSAnalysisApp/utilities/blinder.py | 8 ++++---- .../PWSAnalysisApp/utilities/conglomeratedAnalysis.py | 6 +++--- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py index d5c0dd9..7a487f3 100644 --- a/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py +++ b/src/pwspy_gui/ExtraReflectanceCreator/ERWorkFlow.py @@ -24,7 +24,7 @@ from PyQt5.QtWidgets import QWidget from matplotlib import animation -from pwspy.dataTypes import CameraCorrection, Acquisition, ICMetaData, PwsCube +from pwspy.dataTypes import CameraCorrection, Acquisition, PwsMetaData, PwsCube from pwspy_gui.ExtraReflectanceCreator.widgets.dialog import IndexInfoForm from pwspy.dataTypes import Roi from pwspy import dateTimeFormat @@ -108,7 +108,7 @@ def loadCubes(self, includeSettings: t_.List[str], binning: int, parallelProcess if binning is None: args = {'correction': None, 'binning': None} for cube in df['cube']: - md = ICMetaData.loadAny(cube) + md = PwsMetaData.loadAny(cube) if md.binning is None: raise Exception("No binning metadata found. Please specify a binning setting.") elif md.cameraCorrection is None: @@ -194,7 +194,7 @@ def plot(self, includeSettings: t_.List[str], binning: int, parallelProcessing: print("Select an ROI") roi = cubes['cube'].sample(n=1).iloc[0].selectLassoROI() # Select an ROI to analyze cubeDict = cubes.groupby('setting').apply(lambda df: df.groupby('material')['cube'].apply(list).to_dict()).to_dict() # Transform data frame to a dict of dicts of lists for input to `plot` - self.figs.extend(er.plotExtraReflection(cubeDict, theoryR, matCombos, numericalAperture, roi)) + self.figs.extend(er.plotExtraReflection(cubeDict, theoryR, matCombos, roi)) def save(self, includeSettings: t_.List[str], binning: int, parallelProcessing: bool, numericalAperture: float, parentWidget: QWidget): self.loadIfNeeded(includeSettings, binning, parallelProcessing) diff --git a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/AnalysisSettingsDock/runtimeSettings.py b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/AnalysisSettingsDock/runtimeSettings.py index 2a9a797..4f38982 100644 --- a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/AnalysisSettingsDock/runtimeSettings.py +++ b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/AnalysisSettingsDock/runtimeSettings.py @@ -88,8 +88,8 @@ def getExtraReflectanceMetadata(self) -> pwsdt.ERMetaData: class PWSRuntimeAnalysisSettings(AbstractRuntimeAnalysisSettings): # Inherit docstring settings: PWSAnalysisSettings extraReflectanceMetadata: typing.Optional[pwsdt.ERMetaData] - referenceMetadata: pwsdt.ICMetaData - cellMetadata: typing.List[pwsdt.ICMetaData] + referenceMetadata: pwsdt.PwsMetaData + cellMetadata: typing.List[pwsdt.PwsMetaData] analysisName: str def getSaveableSettings(self) -> PWSAnalysisSettings: @@ -98,10 +98,10 @@ def getSaveableSettings(self) -> PWSAnalysisSettings: def getAnalysisName(self) -> str: return self.analysisName - def getReferenceMetadata(self) -> pwsdt.ICMetaData: + def getReferenceMetadata(self) -> pwsdt.PwsMetaData: return self.referenceMetadata - def getCellMetadatas(self) -> typing.Sequence[pwsdt.ICMetaData]: + def getCellMetadatas(self) -> typing.Sequence[pwsdt.PwsMetaData]: return self.cellMetadata def getExtraReflectanceMetadata(self) -> pwsdt.ERMetaData: diff --git a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/CellSelectorDock/widgets.py b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/CellSelectorDock/widgets.py index 261da51..29931f2 100644 --- a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/CellSelectorDock/widgets.py +++ b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/CellSelectorDock/widgets.py @@ -26,7 +26,7 @@ QInputDialog, QHeaderView from pwspy_gui.PWSAnalysisApp.sharedWidgets import ScrollableMessageBox -from pwspy.dataTypes import Acquisition, ICMetaData, DynMetaData +from pwspy.dataTypes import Acquisition, PwsMetaData, DynMetaData from pwspy_gui.PWSAnalysisApp.sharedWidgets.dictDisplayTree import DictDisplayTreeDialog from pwspy_gui.PWSAnalysisApp.sharedWidgets.tables import NumberTableWidgetItem @@ -349,7 +349,7 @@ def _deleteAnalysisByName(self): else: ret = ScrollableMessageBox.question(self, "Delete Analysis?", f"Are you sure you want to delete {anName} from:" - f"\nPWS: {', '.join([os.path.split(i.acquisitionDirectory.filePath)[-1] for i in deletableCells if isinstance(i, ICMetaData)])}" + f"\nPWS: {', '.join([os.path.split(i.acquisitionDirectory.filePath)[-1] for i in deletableCells if isinstance(i, PwsMetaData)])}" f"\nDynamics: {', '.join([os.path.split(i.acquisitionDirectory.filePath)[-1] for i in deletableCells if isinstance(i, DynMetaData)])}") if ret == QMessageBox.Yes: [i.removeAnalysis(anName) for i in deletableCells] @@ -485,7 +485,7 @@ def updateReferences(self, state: bool, items: t_.List[CellTableWidgetItem]): @property def selectedReferenceMeta(self) -> t_.Optional[Acquisition]: - """Returns the ICMetadata that have been selected. Return None if nothing is selected.""" + """Returns the PwsMetaData that have been selected. Return None if nothing is selected.""" items: List[ReferencesTableItem] = self.selectedItems() assert len(items) <= 1 if len(items) == 0: diff --git a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/__init__.py b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/__init__.py index 658b120..64ad979 100644 --- a/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/__init__.py +++ b/src/pwspy_gui/PWSAnalysisApp/_dockWidgets/ResultsTableDock/__init__.py @@ -30,7 +30,7 @@ from ...componentInterfaces import ResultsTableController if typing.TYPE_CHECKING: - from pwspy.dataTypes import Acquisition, ICMetaData + from pwspy.dataTypes import Acquisition, PwsMetaData from pwspy.analysis.pws import PWSAnalysisSettings from pwspy.analysis.warnings import AnalysisWarning @@ -135,7 +135,7 @@ def _handleCompilationResults(self, inVal: typing.List[typing.Tuple[Acquisition, class CompilationSummaryDisplay(QDialog): - def __init__(self, parent: typing.Optional[QWidget], warnings: typing.List[typing.Tuple[ICMetaData, typing.List[typing.Tuple[PWSRoiCompilationResults, typing.Optional[typing.List[AnalysisWarning]]]]]], analysisName: str = '', analysisSettings: PWSAnalysisSettings = None): + def __init__(self, parent: typing.Optional[QWidget], warnings: typing.List[typing.Tuple[PwsMetaData, typing.List[typing.Tuple[PWSRoiCompilationResults, typing.Optional[typing.List[AnalysisWarning]]]]]], analysisName: str = '', analysisSettings: PWSAnalysisSettings = None): super().__init__(parent=parent) self.setWindowTitle("Compilation Summary") layout = QVBoxLayout() @@ -146,7 +146,7 @@ def __init__(self, parent: typing.Optional[QWidget], warnings: typing.List[typin self._addWarnings(warnings) self.show() - def _addWarnings(self, warnings: typing.List[typing.Tuple[ICMetaData, typing.List[typing.Tuple[ConglomerateCompilerResults, typing.Optional[typing.List[AnalysisWarning]]]]]]): + def _addWarnings(self, warnings: typing.List[typing.Tuple[PwsMetaData, typing.List[typing.Tuple[ConglomerateCompilerResults, typing.Optional[typing.List[AnalysisWarning]]]]]]): for meta, roiList in warnings: item = QTreeWidgetItem(self.warningTree) item.setText(0, meta.filePath) diff --git a/src/pwspy_gui/PWSAnalysisApp/_taskManagers/analysisManager.py b/src/pwspy_gui/PWSAnalysisApp/_taskManagers/analysisManager.py index c335844..49f7651 100644 --- a/src/pwspy_gui/PWSAnalysisApp/_taskManagers/analysisManager.py +++ b/src/pwspy_gui/PWSAnalysisApp/_taskManagers/analysisManager.py @@ -34,7 +34,7 @@ from pwspy.analysis.pws import PWSAnalysis from pwspy_gui.PWSAnalysisApp._dockWidgets.AnalysisSettingsDock.runtimeSettings import PWSRuntimeAnalysisSettings, DynamicsRuntimeAnalysisSettings from pwspy.analysis.warnings import AnalysisWarning -from pwspy.dataTypes import ICRawBase, ICMetaData, DynMetaData +from pwspy.dataTypes import ICRawBase, PwsMetaData, DynMetaData from pwspy.utility.fileIO import loadAndProcess if typing.TYPE_CHECKING: from pwspy_gui.PWSAnalysisApp.App import PWSApp @@ -90,7 +90,7 @@ def runSingle(self, anSettings: AbstractRuntimeAnalysisSettings) -> Tuple[str, A if correctionsOk: if isinstance(anSettings, PWSRuntimeAnalysisSettings): AnalysisClass = PWSAnalysis - refMeta: ICMetaData + refMeta: PwsMetaData elif isinstance(anSettings, DynamicsRuntimeAnalysisSettings): AnalysisClass = DynamicsAnalysis refMeta: DynMetaData @@ -180,7 +180,7 @@ def __init__(self, cellMetas, analysis, anName, cameraCorrection, userSpecifiedB def run(self): try: self.warnings = loadAndProcess(self.cellMetas, processorFunc=self._process, initArgs=[self.analysis, self.anName, self.cameraCorrection, self.userSpecifiedBinning], - parallel=self.parallel, initializer=self._initializer) # Returns a list of Tuples, each tuple containing a list of warnings and the ICmetadata to go with it. + parallel=self.parallel, initializer=self._initializer) # Returns a list of Tuples, each tuple containing a list of warnings and the PwsMetaData to go with it. except Exception as e: import traceback trace = traceback.format_exc() diff --git a/src/pwspy_gui/PWSAnalysisApp/dialogs.py b/src/pwspy_gui/PWSAnalysisApp/dialogs.py index 34a301e..a34fd40 100644 --- a/src/pwspy_gui/PWSAnalysisApp/dialogs.py +++ b/src/pwspy_gui/PWSAnalysisApp/dialogs.py @@ -36,7 +36,7 @@ if typing.TYPE_CHECKING: from typing import Optional, List, Tuple - from pwspy.dataTypes import ICMetaData + from pwspy.dataTypes import PwsMetaData from pwspy.analysis.compilation import PWSRoiCompilationResults from pwspy.analysis.pws import PWSAnalysisSettings from pwspy.analysis.warnings import AnalysisWarning @@ -84,7 +84,7 @@ def show(self): class AnalysisSummaryDisplay(QDialog): - def __init__(self, parent: Optional[QWidget], warnings: List[Tuple[List[AnalysisWarning], ICMetaData]], analysisName: str = '', analysisSettings: PWSAnalysisSettings = None): + def __init__(self, parent: Optional[QWidget], warnings: List[Tuple[List[AnalysisWarning], PwsMetaData]], analysisName: str = '', analysisSettings: PWSAnalysisSettings = None): super().__init__(parent=parent) self.analysisName = analysisName self.analysisSettings = analysisSettings @@ -100,7 +100,7 @@ def __init__(self, parent: Optional[QWidget], warnings: List[Tuple[List[Analysis self.setWindowTitle(f"Analysis Summary: {analysisName}") self.show() - def _addWarnings(self, warnings: List[Tuple[List[AnalysisWarning],ICMetaData]]): + def _addWarnings(self, warnings: List[Tuple[List[AnalysisWarning],PwsMetaData]]): for cellWarns, cell in warnings: item = QTreeWidgetItem(self.warnList) item.setText(0, cell.filePath) diff --git a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_widgets.py b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_widgets.py index 8e5b591..87311c7 100644 --- a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_widgets.py +++ b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_widgets.py @@ -101,7 +101,7 @@ def analysisField(self) -> AnalysisPlotter.PlotFields: def changeData(self, field: _PlotFields): assert isinstance(field, AnalysisPlotter.PlotFields) self._analysisField = field - if field is _PlotFields.Thumbnail: # Load the thumbnail from the ICMetadata object + if field is _PlotFields.Thumbnail: # Load the thumbnail from the PwsMetaData object self._data = self._acq.getThumbnail() elif field in _FluorescencePlotFields: # Open the fluorescence image. idx = _FluorescencePlotFields.index(field) # Get the number for the fluorescence image that has been selected. diff --git a/src/pwspy_gui/PWSAnalysisApp/utilities/blinder.py b/src/pwspy_gui/PWSAnalysisApp/utilities/blinder.py index fcac143..b771087 100644 --- a/src/pwspy_gui/PWSAnalysisApp/utilities/blinder.py +++ b/src/pwspy_gui/PWSAnalysisApp/utilities/blinder.py @@ -22,17 +22,17 @@ from PyQt5.QtWidgets import QDialog, QFileDialog, QWidget, QLineEdit, QPushButton, QLabel, QGridLayout, QMessageBox from pwspy_gui import resources -from pwspy.dataTypes import ICMetaData +from pwspy.dataTypes import PwsMetaData from typing import List import os import random class Blinder: - """A class that, given a list of ICMetadata and the root directory that their files are under, will create randomly + """A class that, given a list of PwsMetaData and the root directory that their files are under, will create randomly numbered symlinks in `outDir` and an index that can be used to trace back to the original. Useful for creating blinded experiments.""" - def __init__(self, cells: List[ICMetaData], homeDir: str, outDir: str): + def __init__(self, cells: List[PwsMetaData], homeDir: str, outDir: str): indexPath = os.path.join(homeDir, 'blindedIndex.json') if os.path.exists(indexPath): raise ValueError(f"A `blindedIndex.json` file already exists in {homeDir}.") @@ -58,7 +58,7 @@ def __init__(self, cells: List[ICMetaData], homeDir: str, outDir: str): class BlinderDialog(QDialog): """This dialog asks the user for the information that is needed in order to perform a blinding with the `Blinder` class.""" - def __init__(self, parent: QWidget, homeDir: str, cells: List[ICMetaData]): + def __init__(self, parent: QWidget, homeDir: str, cells: List[PwsMetaData]): self.parent = parent self.homeDir = homeDir self.cells = cells diff --git a/src/pwspy_gui/PWSAnalysisApp/utilities/conglomeratedAnalysis.py b/src/pwspy_gui/PWSAnalysisApp/utilities/conglomeratedAnalysis.py index adf6fe5..80a5590 100644 --- a/src/pwspy_gui/PWSAnalysisApp/utilities/conglomeratedAnalysis.py +++ b/src/pwspy_gui/PWSAnalysisApp/utilities/conglomeratedAnalysis.py @@ -20,7 +20,7 @@ from pwspy.analysis import warnings from pwspy.analysis.dynamics import DynamicsAnalysisResults from pwspy.analysis.pws import PWSAnalysisResults -from pwspy.dataTypes import Roi +from pwspy.dataTypes import RoiFile from pwspy.analysis.compilation import (DynamicsRoiCompiler, DynamicsCompilerSettings, DynamicsRoiCompilationResults, PWSRoiCompiler, PWSCompilerSettings, PWSRoiCompilationResults, GenericRoiCompiler, GenericCompilerSettings, GenericRoiCompilationResults) @@ -57,11 +57,11 @@ def __init__(self, settings: ConglomerateCompilerSettings): def run(self, results: ConglomerateAnalysisResults, roiFile: RoiFile) -> Tuple[ConglomerateCompilerResults, List[warnings.AnalysisWarning]]: if results.pws is not None: - pwsResults, pwsWarnings = self.pws.run(results.pws, roiFile) + pwsResults, pwsWarnings = self.pws.run(results.pws, roiFile.getRoi()) else: pwsResults, pwsWarnings = None, [] if results.dyn is not None: - dynResults, dynWarnings = self.dyn.run(results.dyn, roiFile) + dynResults, dynWarnings = self.dyn.run(results.dyn, roiFile.getRoi()) else: dynResults, dynWarnings = None, [] genResults = self.generic.run(roiFile) From a6495efc79ee83f6fc8c30d29f6a816b59f94a90 Mon Sep 17 00:00:00 2001 From: Nick Anthony Date: Mon, 6 Dec 2021 12:00:14 -0600 Subject: [PATCH 7/8] update pwspy dependency version --- conda.recipe/meta.yaml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 8dccdcb..f3e7036 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -32,7 +32,7 @@ requirements: - google-auth-httplib2 - google-auth-oauthlib - pyqt =5 - - pwspy >=0.2.8 # Core pws package, available on backmanlab anaconda cloud account. + - pwspy >=0.2.12 # Core pws package, available on backmanlab anaconda cloud account. - mpl_qt_viz >1.0.9 # Plotting package available on PyPi and the backmanlab anaconda cloud account and conda-forge. Written for this project by Nick Anthony - descartes - cachetools >=4 diff --git a/setup.py b/setup.py index aaa89f3..13fa7f7 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ 'google-auth-httplib2', 'google-auth-oauthlib', 'PyQt5', - 'pwspy>=0.2.8', # Core pws package, available on backmanlab anaconda cloud account. + 'pwspy>=0.2.12', # Core pws package, available on backmanlab anaconda cloud account. 'mpl_qt_viz>1.0.9', # Plotting package available on PyPi and the backmanlab anaconda cloud account. Written for this project by Nick Anthony 'descartes', 'cachetools>=4'], From d6cf44b79ed54537f1232912cd7e43c1c868e99f Mon Sep 17 00:00:00 2001 From: Nick Anthony Date: Mon, 6 Dec 2021 12:00:30 -0600 Subject: [PATCH 8/8] Add ROI renaming to GUI --- src/pwspy_gui/PWSAnalysisApp/_roiManager.py | 21 ++++- .../sharedWidgets/plotting/_roiPlot.py | 80 +++++++++++++++++-- 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/pwspy_gui/PWSAnalysisApp/_roiManager.py b/src/pwspy_gui/PWSAnalysisApp/_roiManager.py index 733e541..5b7d1e4 100644 --- a/src/pwspy_gui/PWSAnalysisApp/_roiManager.py +++ b/src/pwspy_gui/PWSAnalysisApp/_roiManager.py @@ -26,7 +26,26 @@ def updateRoi(self, roiFile: pwsdt.RoiFile, roi: pwsdt.Roi): self.roiUpdated.emit(roiFile) def createRoi(self, acq: pwsdt.Acquisition, roi: pwsdt.Roi, roiName: str, roiNumber: int, overwrite: bool = False) -> pwsdt.RoiFile: - roiFile = acq.saveRoi(roiName, roiNumber, roi, overwrite=overwrite) + """ + + Args: + acq: The acquisition to save the ROI to + roi: The ROI to save. + roiName: The name to save the ROI as. + roiNumber: The number to save the ROI as. + overwrite: Whether to overwrite existing ROIs with conflicting name/number combo. + + Returns: + A reference to the created ROIFile + + Raises: + OSError: If `overwrite` is false and an ROIFile for this name and number already exists. + + """ + try: + roiFile = acq.saveRoi(roiName, roiNumber, roi, overwrite=overwrite) + except OSError as e: + raise e self._cache[self._getCacheKey(roiFile)] = roiFile self.roiCreated.emit(roiFile, overwrite) return roiFile diff --git a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py index d63d280..8ef2b63 100644 --- a/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py +++ b/src/pwspy_gui/PWSAnalysisApp/sharedWidgets/plotting/_roiPlot.py @@ -19,7 +19,7 @@ import logging import pickle import re -import typing +import typing as t_ from dataclasses import dataclass from PyQt5.QtCore import pyqtSignal, Qt, QMimeData from shapely.geometry import Polygon as shapelyPolygon @@ -27,7 +27,8 @@ from matplotlib.patches import PathPatch import numpy as np from PyQt5.QtGui import QCursor, QValidator -from PyQt5.QtWidgets import QMenu, QAction, QComboBox, QLabel, QPushButton, QHBoxLayout, QWidget, QVBoxLayout, QApplication, QMessageBox +from PyQt5.QtWidgets import QMenu, QAction, QComboBox, QLabel, QPushButton, QHBoxLayout, QWidget, QVBoxLayout, QApplication, QMessageBox, QInputDialog, QDialog, \ + QGridLayout, QTextEdit, QSpinBox, QLineEdit from PyQt5 import QtCore from pwspy_gui.PWSAnalysisApp.sharedWidgets.plotting._bigPlot import BigPlot from mpl_qt_viz.roiSelection import PolygonModifier, MovingModifier @@ -60,7 +61,7 @@ def __init__(self, Acquisition: pwsdt.Acquisition, data: np.ndarray, roiManager: self.im = self._plotWidget.im self.ax = self._plotWidget.ax - self.rois: typing.List[RoiParams] = [] # This list holds information about the ROIs that are currently displayed. + self.rois: t_.List[RoiParams] = [] # This list holds information about the ROIs that are currently displayed. self.roiFilter = QComboBox(self) self.roiFilter.setEditable(True) @@ -216,6 +217,11 @@ def _keyPressCallback(self, event: KeyEvent): self._editFunc(selected[0]) elif key == 's': # Shift/rotate self._moveRoisFunc() + elif key == 'r': # Rename + sel = [param for param in self.rois if param.selected] + if len(sel) != 1: + return # Only works for one selection + self._renameFunc(sel[0]) def _keyReleaseCallback(self, event: KeyEvent): pass @@ -309,14 +315,21 @@ def pasteFunc(): QMessageBox.information(self, "Nope", 'Pasting Failed. See the log.') logging.getLogger(__name__).exception(e) + popMenu = QMenu(self) deleteAction = popMenu.addAction("Delete Selected ROIs", deleteFunc) moveAction = popMenu.addAction("(S)hift/Rotate Selected ROIs", self._moveRoisFunc) selectAllAction = popMenu.addAction("De/Select (A)ll", self._selectAllFunc) copyAction = popMenu.addAction("Copy ROIs", copyFunc) pasteAction = popMenu.addAction("Paste ROIs", pasteFunc) + # Actions that require that a ROI was clicked on. + popMenu.addSeparator() + modifyAction = popMenu.addAction("(M)odify ROI", lambda sel=selectedROIParam: self._editFunc(sel)) + renameAction = popMenu.addAction("(R)ename ROI", lambda sel=selectedROIParam: self._renameFunc(sel)) + + selectedRoiParams = [r for r in self.rois if r.selected] - if not any([roiParam.selected for roiParam in self.rois]): # If no rois are selected then some actions can't be performed + if not len(selectedRoiParams) == 0: # If no rois are selected then some actions can't be performed deleteAction.setEnabled(False) moveAction.setEnabled(False) copyAction.setEnabled(False) @@ -324,10 +337,12 @@ def pasteFunc(): moveAction.setToolTip(MovingModifier.getHelpText()) popMenu.setToolTipsVisible(True) - if selectedROIParam is not None: - # Actions that require that a ROI was clicked on. - popMenu.addSeparator() - popMenu.addAction("(M)odify", lambda sel=selectedROIParam: self._editFunc(sel)) + if len(selectedRoiParams) == 1: # Only allowed for a single ROI selection + modifyAction.setEnabled(True) + renameAction.setEnabled(True) + else: + modifyAction.setEnabled(False) + renameAction.setEnabled(False) cursor = QCursor() popMenu.popup(cursor.pos()) @@ -392,6 +407,21 @@ def cancelled(): self.enableHoverAnnotation(False) self._polyWidg.initialize([handles]) + def _renameFunc(self, selected: RoiParams): + dlg = RenameDialog(self, selected.roiFile.name, selected.roiFile.number) + result = dlg.exec() + if result != QDialog.Accepted: + return + name, num = dlg.getValues() + + roi = selected.roiFile.getRoi() + try: + self._roiManager.createRoi(acq=self.metadata, roi=roi, roiName=name, roiNumber=num, overwrite=False) + except OSError as e: + QMessageBox.information(self, "Error", "Failed to rename the ROI. Does an ROI with this name and number already exist?") + return # Don't remove the existing roi if we had an issue. + self._roiManager.removeRoi(selected.roiFile) + class WhiteSpaceValidator(QValidator): stateChanged = QtCore.pyqtSignal(QValidator.State) @@ -410,3 +440,37 @@ def validate(self, inp: str, pos: int): def fixup(self, a0: str) -> str: return a0.strip() + +class RenameDialog(QDialog): + def __init__(self, parent: QWidget, initName: str, initNum: int): + super().__init__(parent=parent) + + self._nameField = QLineEdit(initName, self) + self._numberField = QSpinBox(self) + self._numberField.setRange(0, 1000) + self._numberField.setValue(initNum) + + self._okButton = QPushButton("OK", self) + self._cancelButton = QPushButton("Cancel", self) + self._okButton.released.connect(self.accept) + self._cancelButton.released.connect(self.reject) + + l = QGridLayout() + l.addWidget(QLabel("ROI Name:"), 0, 0) + l.addWidget(self._nameField, 0, 1) + l.addWidget(QLabel("ROI #:"), 1, 0) + l.addWidget(self._numberField, 1, 1) + + buttonLayout = QHBoxLayout() + buttonLayout.addWidget(self._okButton) + buttonLayout.addWidget(self._cancelButton) + + ll = QVBoxLayout() + ll.addLayout(l) + ll.addLayout(buttonLayout) + self.setLayout(ll) + + def getValues(self) -> t_.Tuple[str, int]: + if self.result() != QDialog.Accepted: + raise ValueError("Getting dialog values only allowed if the result was accepted.") + return self._nameField.text(), self._numberField.value()