Source code for ccpn.ui.gui.popups.SpectrumGroupEditor

"""
Module Documentation here
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (https://www.ccpn.ac.uk) 2014 - 2022"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
               "Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See https://ccpn.ac.uk/software/licensing/")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Luca Mureddu $"
__dateModified__ = "$dateModified: 2022-02-09 10:41:06 +0000 (Wed, February 09, 2022) $"
__version__ = "$Revision: 3.1.0 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-03-30 11:28:58 +0100 (Thu, March 30, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================

from functools import partial
from PyQt5 import QtWidgets, QtGui, QtCore
from ast import literal_eval
from typing import Tuple, Any
from collections import OrderedDict, Iterable
from ccpn.util.Common import _compareDict
from ccpn.util.Common import makeIterableList, _getObjectsByPids, _getPidsFromObjects
from ccpn.ui.gui.popups.Dialog import handleDialogApply, _verifyPopupApply
from ccpn.core.lib.ContextManagers import undoStackBlocking
from ccpn.core.lib.ContextManagers import queueStateChange
from ccpn.core.Spectrum import Spectrum
from ccpn.core.SpectrumGroup import SpectrumGroup, SeriesTypes
from ccpn.ui.gui.widgets.Tabs import Tabs
from ccpn.ui.gui.widgets.Label import Label
from ccpn.ui.gui.widgets.Frame import Frame
from ccpn.ui.gui.widgets.PulldownListsForObjects import SpectrumGroupPulldown
from ccpn.ui.gui.widgets.Spacer import Spacer
from ccpn.ui.gui.widgets.TextEditor import PlainTextEditor
from ccpn.ui.gui.widgets.RadioButtons import RadioButtons
from ccpn.ui.gui.widgets.CompoundWidgets import PulldownListCompoundWidget
from ccpn.ui.gui.widgets.Icon import Icon
from ccpn.ui.gui.widgets.ButtonList import ButtonList
from ccpn.ui.gui.popups._GroupEditorPopupABC import _GroupEditorPopupABC
from ccpn.ui.gui.popups.SpectrumPropertiesPopup import ColourTab, ContoursTab, Colour1dFrame, ColourNdFrame
from ccpn.util.AttrDict import AttrDict
from ccpn.util.Constants import ALL_SERIES_UNITS
from ccpn.ui.gui.lib.ChangeStateHandler import changeState, ChangeDict
import traceback
import pandas as pd
from collections import defaultdict

DEFAULTSPACING = (3, 3)
TABMARGINS = (1, 10, 1, 5)  # l, t, r, b
INVALIDROWCOLOUR = QtGui.QColor('lightpink')
SPECTRATABNUM = 0
GENERAL1DTABNUM = 2
GENERALNDTABNUM = 1
SERIESTABNUM = 3
NUMTABS = 4
SPECTRA_LABEL = 'Spectra'
GENERALTAB1D_LABEL = 'General 1d'
GENERALTABND_LABEL = 'General Nd'
SERIES_LABEL = 'Series'
MAX_WIDGET_COUNT = 50 # For severe speed issues. If a SG contains more Spectra than this value, widgets are not created.
TAB_WARNING_MSG ='This option is not available for Spectrum Groups containing more than %s spectra.' %MAX_WIDGET_COUNT
_PidsHeader = 'Pids'
_WidgetsHeader = 'Widgets'
_ValuesHeader = 'Values'

[docs]class SpectrumGroupEditor(_GroupEditorPopupABC): """ A popup to create and manage SpectrumGroups Used in 'New' or 'Edit' mode: - For creating new SpectrumGroup (editMode==False); optionally uses passed in spectra list i.e. NewSpectrumGroup of SideBar and Context menu of SideBar - For editing existing SpectrumGroup (editMode==True); requires spectrumGroup argument i.e. Edit of SpectrumGroup of SideBar or For selecting and editing SpectrumGroup (editMode==True) i.e. Menu Spectrum->Edit SpectrumGroup... """ _class = SpectrumGroup _classItemAttribute = 'spectra' # Attribute in _class containing items _classPulldown = SpectrumGroupPulldown _projectNewMethod = 'newSpectrumGroup' # Method of Project to create new _class instance _projectItemAttribute = 'spectra' # Attribute of Project containing items _pluralGroupName = 'Spectrum Groups' _singularGroupName = 'Spectrum Group' _groupPidKey = 'SG' _itemPidKey = 'SP' _setRevertButton = False def _applyChanges(self): """ The apply button has been clicked Define an undo block for setting the properties of the object If there is an error setting any values then generate an error message If anything has been added to the undo queue then remove it with application.undo() repopulate the popup widgets This is controlled by a series of dicts that contain change functions - operations that are scheduled by changing items in the popup. These functions are executed when the Apply or OK buttons are clicked Return True unless any errors occurred """ _colourTabs1d = [self._colourTabs1d.widget(tt) for tt in range(self._colourTabs1d.count())] _colourTabsNd = [self._colourTabsNd.widget(tt) for tt in range(self._colourTabsNd.count())] # if not (_colourTabs1d or _colourTabsNd): # raise RuntimeError("Code error: tabs not implemented") _allTabs = self.getActiveTabList() # get the list of widgets that have been changed - exit if all empty allChanges = any(t._changes for t in _allTabs if t is not None) or (True if self._currentEditorState() else False) if not allChanges: return True # handle clicking of the Apply/OK button with handleDialogApply(self) as error: # get the list of spectra that have changed - for refreshing the displays spectrumList = [] for t in (_colourTabs1d + _colourTabsNd): if t is not None: changes = t._changes if changes: spectrumList.append(t.spectrum) # add an undo item to redraw these spectra with undoStackBlocking() as addUndoItem: addUndoItem(undo=partial(self._updateGl, spectrumList)) # call the _groupEditor _applyChanges method which sets the group items if not super()._applyChanges(): error.errorValue = True # add a redo item to redraw these spectra with undoStackBlocking() as addUndoItem: addUndoItem(redo=partial(self._updateGl, spectrumList)) # rebuild the contours as required for spec in spectrumList: for specViews in spec.spectrumViews: specViews.buildContours = True self._updateGl(spectrumList) # everything has happened - disable the apply button self._applyButton.setEnabled(False) # check for any errors if error.errorValue: # repopulate popup on an error self._populate() return False # remove all changes for tab in (_colourTabs1d + _colourTabsNd): tab._changes = ChangeDict() # self._currentNumApplies += 1 # self._revertButton.setEnabled(True) return True def _groupInit(self): # apply the changes to the listed spectra spectraTab._changes if self.spectraTab._changes: self._applyAllChanges(self.spectraTab._changes) for tt in range(self._colourTabs1d.count()): _changes = self._colourTabs1d.widget(tt)._changes if _changes: self._applyAllChanges(_changes) for tt in range(self._colourTabsNd.count()): _changes = self._colourTabsNd.widget(tt)._changes if _changes: self._applyAllChanges(_changes) try: _changes = self._group1dColours._changes if _changes: # self._group1dColours.spectrumGroup = self.obj self._applyAllChanges(_changes) if self.obj != self._group1dColours.spectrumGroup: # if a dummy spectrumGroup then copy to actual group for k, val in self._group1dColours.spectrumGroup.items(): setattr(self.obj, k, val) except: pass try: _changes = self._groupNdColours._changes if _changes: # self._groupNdColours.spectrumGroup = self.obj self._applyAllChanges(_changes) if self.obj != self._groupNdColours.spectrumGroup: # if a dummy spectrumGroup then copy to actual group for k, val in self._groupNdColours.spectrumGroup.items(): setattr(self.obj, k, val) except Exception as es: pass self._spectrumGroupSeriesEdited = OrderedDict() self._spectrumGroupSeriesValues = list(self.obj.series) self._spectrumGroupSeriesUnitsEdited = None self._spectrumGroupSeriesTypeEdited = None # set the series values - this may crash if self.seriesTab._changes: self._applyAllChanges(self.seriesTab._changes) if self._spectrumGroupSeriesUnitsEdited is not None: self.obj.seriesUnits = self._spectrumGroupSeriesUnitsEdited if self._spectrumGroupSeriesTypeEdited is not None: self.obj.seriesType = self._spectrumGroupSeriesTypeEdited if self.obj.id in self._currentEditorState(): specList = self._currentEditorState()[self.obj.id].get('spectra') or [] for dim, specPid in enumerate(specList): spec = self.project.getByPid(specPid) # read the value from the edits dict - bit of a hack from _changeSpectrumSeriesValues if spec and dim in self._spectrumGroupSeriesEdited: self._spectrumGroupSeriesValues[dim] = self._spectrumGroupSeriesEdited[dim] if self._spectrumGroupSeriesEdited: try: for ii, val in enumerate(self._spectrumGroupSeriesValues): try: tp = self.seriesTab.seriesType.getIndex() if tp == SeriesTypes.FLOAT.value: val = float(val) elif tp == SeriesTypes.INTEGER.value: val = int(val) elif tp == SeriesTypes.STRING.value: val = str(val) else: val = repr(val) except Exception as es: break else: self._spectrumGroupSeriesValues[ii] = val else: self.obj.series = tuple(self._spectrumGroupSeriesValues) except Exception as es: raise es _groupEditorInitMethod = _groupInit # make this a tabbed dialog, with the default widget going into tab 0 _useTab = 0 _numberTabs = 4 # create the first tab def __init__(self, parent=None, mainWindow=None, editMode=True, obj=None, defaultItems=None, size=(700, 550), **kwds): """ Initialise the widget, note defaultItems is only used for create """ super().__init__(parent=parent, mainWindow=mainWindow, editMode=editMode, obj=obj, defaultItems=defaultItems, size=size, **kwds) self.TAB_NAMES = ((SPECTRA_LABEL, self._initSpectraTab), (GENERALTABND_LABEL, self._initGeneralTabNd), (GENERALTAB1D_LABEL, self._initGeneralTab1d), (SERIES_LABEL, self._initSeriesTab)) if obj and editMode: defaultItems = obj.spectra # replace the tab widget with a new seriesWidget seriesTabContents = SeriesFrame(parent=self, mainWindow=self.mainWindow, spectrumGroup=obj, editMode=editMode, showCopyOptions=True if defaultItems and len(defaultItems) > 1 else False, defaultItems=defaultItems or ()) self._tabWidget.widget(SERIESTABNUM).setWidget(seriesTabContents) # get pointers to the tabs self.spectraTab = self._tabWidget.widget(SPECTRATABNUM)._scrollContents self.generalTab1d = self._tabWidget.widget(GENERAL1DTABNUM)._scrollContents self.generalTabNd = self._tabWidget.widget(GENERALNDTABNUM)._scrollContents self.seriesTab = self._tabWidget.widget(SERIESTABNUM)._scrollContents self._generalTabWidget1d = self._tabWidget.widget(GENERAL1DTABNUM) self._generalTabWidgetNd = self._tabWidget.widget(GENERALNDTABNUM) self.currentSpectra = self._getSpectraFromList() # this should be the list when the popup is opened self._defaultSpectra = self.currentSpectra self._defaultName = self._editedObject.name if self._editedObject else '' self._defaultComment = self._editedObject.comment if self._editedObject else '' # set the labels in the first pass for tNum, (tabName, tabFunc) in enumerate(self.TAB_NAMES): self._tabWidget.setTabText(tNum, tabName) # call the tab initialise functions (may show/hide tabs) for tNum, (tabName, tabFunc) in enumerate(self.TAB_NAMES): if tabFunc: tabFunc() self._populate() # is not needed to populate again all. self.setDefaultButton(None) self.connectSignals() self.setSizeGripEnabled(False)
[docs] def connectSignals(self): # connect to changes in the spectrumGroup self.nameEdit.textChanged.connect(self.seriesTab._queueChangeName) self.commentEdit.textChanged.connect(self.seriesTab._queueChangeComment) self.leftListWidget.model().dataChanged.connect(self._spectraChanged) self.leftListWidget.model().rowsRemoved.connect(self._spectraChanged) self.leftListWidget.model().rowsInserted.connect(self._spectraChanged) self.leftListWidget.model().rowsMoved.connect(self._spectraChanged)
[docs] def getActiveTabList(self): """Return the list of active tabs """ # test the colour tabs for the moment _1dTabs = tuple(self._colourTabs1d.widget(ii) for ii in range(self._colourTabs1d.count())) _NdTabs = tuple(self._colourTabsNd.widget(ii) for ii in range(self._colourTabsNd.count())) _1dGroup = (self._group1dColours,) if _1dTabs else () _NdGroup = (self._groupNdColours,) if _NdTabs else () tabs = _1dTabs + _NdTabs + _1dGroup + _NdGroup + (self.spectraTab, self.seriesTab) return tabs
def _initSpectraTab(self): thisTab = self.spectraTab thisTab._changes = ChangeDict() def _init1DColourTabs(self): spectra1d = [spec for spec in (self.currentSpectra or []) if spec.dimensionCount == 1] for specNum, thisSpec in enumerate(spectra1d): if thisSpec.dimensionCount == 1: contoursTab = ColourTab(parent=self, mainWindow=self.mainWindow, spectrum=thisSpec, showCopyOptions=True if len(spectra1d) > 1 else False, copyToSpectra=spectra1d) self._colourTabs1d.addTab(contoursTab, thisSpec.name) contoursTab.setContentsMargins(*TABMARGINS) self._colourTabs1d.setTabClickCallback(self._tabClicked1d) self._colourTabs1d.tabCloseRequested.connect(self._closeColourTab1d) index = self._tabWidget.indexOf(self._generalTabWidget1d) if self._colourTabs1d.count() == 0: if (0 <= index < NUMTABS): self._tabWidget.removeTab(index) def _initGeneralTab1d(self): thisTab = self.generalTab1d self._group1dColours = Colour1dFrame(parent=thisTab, mainWindow=self.mainWindow, container=self, editMode=self.editMode, spectrumGroup=self.obj, grid=(0, 0), setLayout=True) self._colourDisabledFrame = Frame(thisTab, setLayout=True, showBorder=False, grid=(1, 0), vAlign='t') iconLabel = Label(self._colourDisabledFrame, icon=Icon('icons/exclamation_small'), grid=(0, 0), hAlign='l') tabWarningLabel = Label(self._colourDisabledFrame, text=TAB_WARNING_MSG, grid=(0, 1), hAlign='l', ) self._colourDisabledFrame.hide() self._colourTabs1d = Tabs(thisTab, grid=(2, 0)) self._oldTabs = OrderedDict() self._group1dColours.setContentsMargins(5, 5, 5, 5) thisTab.setContentsMargins(0, 0, 0, 0) # remember the state when switching tabs self.copyCheckBoxState = [] if len(self.currentSpectra) > MAX_WIDGET_COUNT: self._setDisabledColourTab() else: self._init1DColourTabs() def _initGeneralTabNd(self): thisTab = self.generalTabNd self._groupNdColours = ColourNdFrame(parent=thisTab, mainWindow=self.mainWindow, container=self, editMode=self.editMode, spectrumGroup=self.obj, grid=(0, 0), setLayout=True) self._colourTabsNd = Tabs(thisTab, grid=(1, 0)) self._groupNdColours.setContentsMargins(5, 5, 5, 5) thisTab.setContentsMargins(0, 0, 0, 0) # remember the state when switching tabs self.copyCheckBoxState = [] spectraNd = [spec for spec in (self.currentSpectra or []) if spec.dimensionCount > 1] for specNum, thisSpec in enumerate(spectraNd): if thisSpec.dimensionCount > 1: contoursTab = ContoursTab(parent=self, mainWindow=self.mainWindow, spectrum=thisSpec, showCopyOptions=True if len(spectraNd) > 1 else False, copyToSpectra=spectraNd) self._colourTabsNd.addTab(contoursTab, thisSpec.name) contoursTab.setContentsMargins(*TABMARGINS) self._colourTabsNd.setTabClickCallback(self._tabClickedNd) self._colourTabsNd.tabCloseRequested.connect(self._closeColourTabNd) index = self._tabWidget.indexOf(self._generalTabWidgetNd) if self._colourTabsNd.count() == 0: if (0 <= index < NUMTABS): self._tabWidget.removeTab(index) def _initSeriesTab(self): thisTab = self.seriesTab thisTab._changes = ChangeDict() thisTab._populateSeries() def _fillPullDowns(self): for colourTab in (self._colourTabs1d, self._colourTabsNd): for aTab in tuple(colourTab.widget(ii) for ii in range(colourTab.count())): aTab._fillPullDowns() try: self._group1dColours._fillPullDowns() except: pass try: self._groupNdColours._fillPullDowns() except: pass def _populate(self): """Populate the widgets in the tabs """ ## NOTE:ED - check that the widgets are populated correctly - may be called exponentially from not ## blocking the notification-change super()._populate() with self.seriesTab._changes.blockChanges(): self._spectraChanged() # check whether any tabs need removing here for colourTab in (self._colourTabs1d, self._colourTabsNd): for aTab in tuple(colourTab.widget(ii) for ii in range(colourTab.count())): aTab._populateColour() try: self._group1dColours._populateColour() except: pass try: self._groupNdColours._populateColour() except: pass with self.blockWidgetSignals(): # we already filled when calling _spectraChanged self.seriesTab._fillSeriesFrame(self._defaultSpectra, spectrumGroup=self.obj) self.seriesTab._populateSeries() def _getChangeState(self): """Get the change state from the _changes dict """ try: editName = self.nameEdit.text() defaultName = self._defaultName editComment = self.commentEdit.text() or None defaultComment = self._defaultComment pidState = self._groupedObjects pidList = [str(spec.pid) for spec in self._defaultSpectra] revertState = (pidState != pidList) or (editName != defaultName) or (editComment != defaultComment) # applyState = (True if pidState else False) and (True if editName else False) # and revertState # only need a name, can now have an empty group applyState = (True if editName else False) # and revertState tabs = self.getActiveTabList() allChanges = any(t._changes for t in tabs if t is not None) except Exception as es: return None return changeState(self, allChanges, applyState, revertState, None, self._applyButton, self._revertButton, 0) def _tabClicked1d(self, index): """Callback for clicking a tab - needed for refilling the checkboxes and populating the pulldown """ for colourTab in (self._colourTabs1d,): aTabs = tuple(colourTab.widget(ii) for ii in range(colourTab.count())) if aTabs and hasattr(aTabs[index], '_populateCheckBoxes'): aTabs[index]._populateCheckBoxes() def _tabClickedNd(self, index): """Callback for clicking a tab - needed for refilling the checkboxes and populating the pulldown """ for colourTab in (self._colourTabsNd,): aTabs = tuple(colourTab.widget(ii) for ii in range(colourTab.count())) if aTabs and hasattr(aTabs[index], '_populateCheckBoxes'): aTabs[index]._populateCheckBoxes() def _getSpectraFromList(self): """Get the list of spectra from the list """ # spectra = _getObjectsByPids() spectra = [self.project.getByPid(spectrum) if isinstance(spectrum, str) else spectrum for spectrum in self._groupedObjects] return [spec for spec in spectra if spec] def _cleanColourTab(self, spectrum): """Remove the unwanted queue items from spectra reomved from the spectrumQueue """ with self.blockWidgetSignals(): for colourTab in (self._colourTabs1d, self._colourTabsNd): for aTab in tuple(colourTab.widget(ii) for ii in range(colourTab.count())): if aTab.spectrum == spectrum: aTab._cleanWidgetQueue() def _removeTab(self, spectrum): """Remove the unwanted queue items from spectra reomved from the spectrumQueue """ for colourTab in (self._colourTabs1d, self._colourTabsNd): for aTab in tuple(colourTab.widget(ii) for ii in range(colourTab.count())): if aTab.spectrum == spectrum: pass def _closeColourTab1d(self, index): self._colourTabs1d.removeTab(index) def _closeColourTabNd(self, index): self._colourTabsNd.removeTab(index) def _leftPullDownCallback(self, value=None): """Callback when selecting the left spectrumGroup pulldown item """ obj = self.project.getByPid(value) if obj: # set the new object self.defaultObject = obj super(SpectrumGroupEditor, self)._leftPullDownCallback(value) def _spectraChanged(self, *args): """Respond to a change in the list of spectra to add the spectrumGroup """ self._spectraChanged1d() self._spectraChangedNd() # call the series frame as this contains code for updating _changes self.seriesTab._queueChangeSpectrumList() self.seriesTab._fillSeriesFrame(self.currentSpectra, spectrumGroup=self.obj) def _spectraChanged1d(self): self._newSpectra = self._getSpectraFromList() if len(self._newSpectra)>MAX_WIDGET_COUNT: self._setDisabledColourTab() self.seriesTab._setDisabledSeriesTab() return self._colourTabs1d.show() self._colourDisabledFrame.hide() self.seriesTab._seriesEnabled = True deleteSet = (set(self.currentSpectra) - set(self._newSpectra)) newSet = (set(self.currentSpectra) - set(self._newSpectra)) # only select the 1d spectra deleteSet = [spec for spec in deleteSet if spec.dimensionCount == 1] spectra1d = [spec for spec in self._newSpectra if spec.dimensionCount == 1] for spec in deleteSet: # remove tab widget self._cleanColourTab(spec) if spec in self.seriesTab._currentSeriesValues: del self.seriesTab._currentSeriesValues[spec] # why this del here # remove all in reverse order, keep old ones for ii in range(self._colourTabs1d.count() - 1, -1, -1): tab = self._colourTabs1d.widget(ii) self._oldTabs[tab.spectrum] = tab self._colourTabs1d.removeTab(ii) for spec in spectra1d: # add new tab widget here if spec in self._oldTabs: self._colourTabs1d.addTab(self._oldTabs[spec], spec.name) self._oldTabs[spec].setCopyOptionsVisible(True if len(spectra1d) > 1 else False) # make sure the new spectrum lists are up-to-date self._oldTabs[spec]._updateSpectra(spec, spectra1d) self._oldTabs[spec]._populateColour() else: if spec.dimensionCount == 1: contoursTab = ColourTab(parent=self, mainWindow=self.mainWindow, spectrum=spec, showCopyOptions=True if len(spectra1d) > 1 else False, copyToSpectra=spectra1d) self._colourTabs1d.addTab(contoursTab, spec.name) #this indent looks wrong contoursTab.setContentsMargins(*TABMARGINS) contoursTab._populateColour() # set the visibility of the general 1d tab index = self._tabWidget.indexOf(self._generalTabWidget1d) if self._colourTabs1d.count() == 0: if (0 <= index < NUMTABS): self._tabWidget.removeTab(index) else: if not (0 <= index < NUMTABS): self._tabWidget.insertTab(1, self._generalTabWidget1d, GENERALTAB1D_LABEL) # update the current list self.currentSpectra = self._newSpectra def _spectraChangedNd(self): self._newSpectra = self._getSpectraFromList() deleteSet = (set(self.currentSpectra) - set(self._newSpectra)) newSet = (set(self.currentSpectra) - set(self._newSpectra)) # only select the 1d spectra deleteSet = [spec for spec in deleteSet if spec.dimensionCount > 1] spectraNd = [spec for spec in self._newSpectra if spec.dimensionCount > 1] for spec in deleteSet: # remove tab widget self._cleanColourTab(spec) if spec in self.seriesTab._currentSeriesValues: del self.seriesTab._currentSeriesValues[spec] # remove all in reverse order, keep old ones for ii in range(self._colourTabsNd.count() - 1, -1, -1): tab = self._colourTabsNd.widget(ii) self._oldTabs[tab.spectrum] = tab self._colourTabsNd.removeTab(ii) for spec in spectraNd: # add new tab widget here if spec in self._oldTabs: self._colourTabsNd.addTab(self._oldTabs[spec], spec.name) self._oldTabs[spec].setCopyOptionsVisible(True if len(spectraNd) > 1 else False) # make sure the new spectrum lists are up-to-date self._oldTabs[spec]._updateSpectra(spec, spectraNd) self._oldTabs[spec]._populateColour() else: if spec.dimensionCount > 1: contoursTab = ContoursTab(parent=self, mainWindow=self.mainWindow, spectrum=spec, showCopyOptions=True if len(spectraNd) > 1 else False, copyToSpectra=spectraNd) self._colourTabsNd.addTab(contoursTab, spec.name) contoursTab.setContentsMargins(*TABMARGINS) contoursTab._populateColour() # set the visibility of the general 1d tab index = self._tabWidget.indexOf(self._generalTabWidgetNd) if self._colourTabsNd.count() == 0: if (0 <= index < NUMTABS): self._tabWidget.removeTab(index) else: if not (0 <= index < NUMTABS): self._tabWidget.insertTab(self._tabWidget.count() - 1, self._generalTabWidgetNd, GENERALTABND_LABEL) # update the current list self.currentSpectra = self._newSpectra
[docs] def copySpectra(self, fromSpectrum, toSpectra): """Copy the contents of tabs to other spectra """ colourTabs = [self._colourTabs1d.widget(ii) for ii in range(self._colourTabs1d.count())] + \ [self._colourTabsNd.widget(ii) for ii in range(self._colourTabsNd.count())] for aTab in colourTabs: if aTab.spectrum == fromSpectrum: fromSpectrumTab = aTab for aTab in [tab for tab in colourTabs if tab != fromSpectrumTab and tab.spectrum in toSpectra]: aTab._copySpectrumAttributes(fromSpectrumTab)
def _setDisabledColourTab(self): self._colourTabs1d.hide() self._colourDisabledFrame.show() for sp in self.currentSpectra: self._cleanColourTab(sp)
[docs]class SeriesFrame(Frame): _editMode = False def __init__(self, parent=None, mainWindow=None, spectrumGroup=None, editMode=False, showCopyOptions=False, defaultItems=None): super().__init__(parent, setLayout=True, spacing=DEFAULTSPACING) self._parent = parent self.mainWindow = mainWindow self.application = mainWindow.application self.preferences = self.application.preferences self._editMode = editMode self._seriesEnabled = True # check that the spectrum and the copyToSpectra list are correctly defined getByPid = self.application.project.getByPid self.spectrumGroup = getByPid(spectrumGroup) if isinstance(spectrumGroup, str) else spectrumGroup if not isinstance(self.spectrumGroup, (SpectrumGroup, type(None))): raise TypeError('spectrumGroup must be of type spectrumGroup or None') if not isinstance(defaultItems, (Iterable, type(None))): raise TypeError('copyToSpectra must be of type Iterable/None') if defaultItems: self._copyToSpectra = [getByPid(spectrum) if isinstance(spectrum, str) else spectrum for spectrum in defaultItems] for spec in self._copyToSpectra: if not isinstance(spec, (Spectrum, type(None))): raise TypeError('copyToSpectra is not defined correctly.') else: self._copyToSpectra = None self._changes = ChangeDict() self._editors = OrderedDict() self._currentSeriesValues = OrderedDict() if self._editMode: self.defaultObject = spectrumGroup else: # create a dummy object that SHOULD contain the required attributes self.defaultObject = _SpectrumGroupContainer() self.defaultObject.spectra = defaultItems self._row = 0 self._col = 0 # seriesLabel = Label(self, text="Spectrum SeriesValues", grid=(self._row, self._col), gridSpan=(1, 3), hAlign='l') # seriesLabel.setFixedHeight(30) # seriesLabel.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) Spacer(self, 10, 10, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed, grid=(self._row, self._col)) self._row += 1 self._seriesDisabledFrame = Frame(self, setLayout=True, showBorder=False, grid=(self._row, self._col), vAlign='t') iconLabel = Label(self._seriesDisabledFrame, icon=Icon('icons/exclamation_small'), grid=(0, 0), hAlign='l') tabWarningLabel = Label(self._seriesDisabledFrame, text=TAB_WARNING_MSG, grid=(0, 1), hAlign='l', ) self._seriesDisabledFrame.hide() self._seriesOptionsFrame = Frame(self, setLayout=True, showBorder=False, grid=(self._row, self._col), vAlign='t') self.seriesTypeLabel = Label(self._seriesOptionsFrame, text='Series Type', grid=(self._row, self._col), hAlign='l') self.seriesType = RadioButtons(self._seriesOptionsFrame, texts=[str(val.description) for val in SeriesTypes], grid=(self._row, self._col + 1), gridSpan=(1, 2), hAlign='l', callback=partial(self._queueChangeSeriesType, self.defaultObject)) self._row += 1 self._seriesFrameRow = self._row self._seriesFrameCol = self._col # self._seriesFrame = Frame(self, setLayout=True, showBorder=False, grid=(self._seriesFrameRow, self._seriesFrameCol), gridSpan=(1, 3)) self._seriesFrame = None self._row += 1 # unitsLabel = Label(self, text='Series Units', grid=(self._row, self._col), hAlign='l') # self.unitsEditor = LineEdit(self, grid=(self._row, self._col + 1)) # unitsLabel.setFixedHeight(30) reorderLabel = Label(self, text='Reorder spectra by series', grid=(self._row, self._col), hAlign='l') self._orderButtons = ButtonList(self, texts=['Ascending','Descending'], icons=[Icon('icons/sort-up'),Icon('icons/sort-down')], callbacks=[partial(self._reorderSpectraBySeries, True), partial(self._reorderSpectraBySeries, False)], grid=(self._row, self._col + 1)) self._row += 1 self.unitsEditor = PulldownListCompoundWidget(self._seriesOptionsFrame, labelText='Series Units', grid=(self._row, self._col), gridSpan=(1, 3), hAlign='l', editable=True, sizeAdjustPolicy=QtWidgets.QComboBox.AdjustToContents) self.unitsEditor.pulldownList.pulldownTextEdited.connect(partial(self._queueChangeSeriesUnits, self.unitsEditor, self.defaultObject)) self.unitsEditor.pulldownList.pulldownTextReady.connect(partial(self._updateSeriesUnitsPulldown, self.unitsEditor, self.defaultObject)) self._pulldownData = ALL_SERIES_UNITS self._row += 1 self._errorFrameSeriesValues = Frame(self._seriesOptionsFrame, setLayout=True, grid=(self._row, self._col), gridSpan=(1, 3), hAlign='l') # add a frame containing an error message if the series values are not all the same type self.errorIcon = Icon('icons/exclamation_small') # self._errorFrameSeriesValues.layout().setColumnStretch(0, 1) # self._errorFrameSeriesValues.layout().setColumnStretch(1, 1000) self._errors = ['seriesValues must be of the same type'] for i, error in enumerate(self._errors): iconLabel = Label(self._errorFrameSeriesValues, grid=(i, 0)) iconLabel.setPixmap(self.errorIcon.pixmap(16, 16)) label = Label(self._errorFrameSeriesValues, error, grid=(i, 1)) self._errorFrameSeriesValues.hide() self._row += 1 self._errorFrameDict = Frame(self._seriesOptionsFrame, setLayout=True, grid=(self._row, self._col), gridSpan=(1, 3), hAlign='l') # add a frame containing an error message if the dicts do not contain the same keys self.errorIcon = Icon('icons/exclamation_small') # self._errorFrameDict.layout().setColumnStretch(0, 1) # self._errorFrameDict.layout().setColumnStretch(1, 1000) self._errors = ['seriesValue dicts do not contain the same keys'] for i, error in enumerate(self._errors): iconLabel = Label(self._errorFrameDict, grid=(i, 0)) iconLabel.setPixmap(self.errorIcon.pixmap(16, 16)) label = Label(self._errorFrameDict, error, grid=(i, 1)) self._errorFrameDict.hide() self._row += 1 Spacer(self._seriesOptionsFrame, 1, 1, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding, grid=(self._row, self._col + 2)) self._seriesOptionsFrame.getLayout().setRowStretch(self._row, 10) # set the background to transparent so matches the colour of the tab self.setAutoFillBackground(False) self.setStyleSheet('Frame { background: transparent; }') if len(defaultItems) > MAX_WIDGET_COUNT: self._setDisabledSeriesTab() if self._seriesEnabled: self._fillSeriesFrame(defaultItems=defaultItems, spectrumGroup=self.defaultObject) # get colours from the lineEdit and copy to the plainTextEdit # yourWidget.palette().highlight().color().name()? def _getDFfromSeriesWidgets(self): dd = defaultdict(list) for sp, widget in self._editors.items(): dd[_WidgetsHeader].append(widget) dd[_ValuesHeader].append(widget.get()) dd[_PidsHeader].append(sp.pid) df = pd.DataFrame(dd) return df def _reorderSpectraBySeries(self, ascending=True): """ Sort on-the-fly all widgets based on Series lineEdit values. To sort a spectrumGroup object by Series (using the terminal), use the method "SpectrumGroup > sortSpectraBySeries() " """ selType = self.seriesType.get() isNumericTab = selType == SeriesTypes.INTEGER.description or selType == SeriesTypes.FLOAT.description df = self._getDFfromSeriesWidgets() if isNumericTab: # set _ValuesHeader to numeric and ignore potential errors (empty boxes, or str conversion) df[_ValuesHeader] = pd.to_numeric(df[_ValuesHeader], errors='coerce') # errors='coerce' is essential here! sortedDf = df.sort_values(by=_ValuesHeader, ascending=ascending) # do sorting for i, row in sortedDf.iterrows(): widget = row[_WidgetsHeader] widget.set(str(row[_ValuesHeader])) self._parent._groupedObjects = list(sortedDf[_PidsHeader].values) self.seriesType.set(selType) def _setDisabledSeriesTab(self): self._seriesDisabledFrame.show() self._seriesEnabled = False if self._seriesFrame: self._seriesFrame.hide() self._seriesFrame.deleteLater() self._seriesOptionsFrame.hide() def _fillSeriesFrame(self, defaultItems, spectrumGroup=None): """Reset the contents of the series frame for changed spectrum list """ if not self._seriesEnabled: return # remove previous editor values # print(traceback.print_stack()) self._seriesDisabledFrame.hide() self._seriesOptionsFrame.show() for spec, editor in self._editors.items(): editor.textChanged.disconnect() self._currentSeriesValues[spec] = editor.get() self._changes.clear() self._editors = OrderedDict() self.defaultObject = spectrumGroup with self._changes.blockChanges(): # empty the frame if self._seriesFrame: self._seriesFrame.hide() self._seriesFrame.deleteLater() self._seriesFrame = Frame(self, setLayout=True, showBorder=False, grid=(self._seriesFrameRow, self._seriesFrameCol), gridSpan=(1, 3), vAlign='t') self.getLayout().setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) # add new editors with the new values for sRow, spec in enumerate(defaultItems): seriesLabel = Label(self._seriesFrame, text=spec.pid, grid=(sRow, 0), vAlign='t') seriesLabel.setFixedHeight(30) editorFrame = Frame(self._seriesFrame, setLayout=True, grid=(sRow, 1), vAlign='t') seriesEditor = PlainTextEditor(editorFrame, grid=(0, 0), fitToContents=True) seriesEditor.setMinimumSize(50, 25) # attributes for setting size when using resize-grip seriesEditor._minimumWidth = 50 seriesEditor._minimumHeight = 25 seriesEditor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) if spec in self._currentSeriesValues and self._currentSeriesValues[spec] is not None: # print('filling seriesEditor value: %s' % self._currentSeriesValues[spec]) seriesEditor.set(self._currentSeriesValues[spec]) # add the callback after setting the initial values seriesEditor.textChanged.connect(partial(self._queueChangeSpectrumSeriesValues, seriesEditor, self.defaultObject, spec, sRow)) self._editors[spec] = seriesEditor self._seriesFrame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) def _populateSeries(self): """Populate the texteditors - seriesValues and seriesUnits for the spectrumGroup """ if not self._seriesEnabled: return if self.defaultObject: with self._changes.blockChanges(): self.seriesType.setIndex(int(self.defaultObject.seriesType or 0)) series = self.defaultObject.series if series: for spec, textEditor in self._editors.items(): try: ii = self.defaultObject.spectra.index(spec) except: pass try: if self.seriesType.getIndex() == SeriesTypes.FLOAT.value: seriesValue = float(series[ii]) if self.seriesType.getIndex() == SeriesTypes.INTEGER.value: seriesValue = int(series[ii]) elif self.seriesType.getIndex() == SeriesTypes.STRING.value: seriesValue = str(series[ii]) else: seriesValue = repr(series[ii]) except Exception as es: textEditor.set('') else: # print('populating textEditor value: %s' % seriesValue) textEditor.set(str(seriesValue)) if self.defaultObject.seriesUnits is not None and self.defaultObject.seriesUnits not in self._pulldownData: self._pulldownData += (self.defaultObject.seriesUnits,) self.unitsEditor.modifyTexts(texts=self._pulldownData) self.unitsEditor.select(self.defaultObject.seriesUnits) self._validateEditors() def _getValuesFromTextEdit(self): # read the values from the textEditors and put in dict for later for spec, editor in self._editors.items(): self._currentSeriesValues[spec] = editor.get() def _cleanSpacers(self, widget): # remove unneeded spacers layout = widget.getLayout() spacers = [layout.itemAt(itmNum) for itmNum in range(layout.count()) if isinstance(layout.itemAt(itmNum), Spacer)] for sp in spacers: layout.removeItem(sp) @queueStateChange(_verifyPopupApply) def _queueChangeSpectrumSeriesValues(self, editor, spectrumGroup, spectrum, dim): # queue the value if has changed from the original value = editor.get() try: if self.seriesType.getIndex() == SeriesTypes.FLOAT.value: seriesValue = float(value) elif self.seriesType.getIndex() == SeriesTypes.INTEGER.value: seriesValue = int(value) elif self.seriesType.getIndex() == SeriesTypes.STRING.value: seriesValue = str(value) else: seriesValue = literal_eval(value) except Exception as es: # return partial(self._changeSpectrumSeriesValues, spectrumGroup, spectrum, dim, ERRORSTRING) pass else: specValue = spectrum._getSeriesItemsById(spectrumGroup.pid) if spectrumGroup else None if seriesValue != specValue: return partial(self._changeSpectrumSeriesValues, spectrumGroup, spectrum, dim, seriesValue) finally: self._validateEditors() def _validateEditors(self): """Check that all the editors contain the same type of seriesValues """ errorState = False errorDict = False heightSum = 0.0 literalTypes = set() literalDictCompare = None for editor in self._editors.values(): value = editor.get() palette = editor.viewport().palette() colour = editor._background try: seriesValue = None if self.seriesType.getIndex() == SeriesTypes.FLOAT.value: seriesValue = float(value) elif self.seriesType.getIndex() == SeriesTypes.INTEGER.value: seriesValue = int(value) elif self.seriesType.getIndex() == SeriesTypes.STRING.value: seriesValue = str(value) else: seriesValue = literal_eval(value) literalTypes.add(type(seriesValue)) # compare whether the dicts contain the same keys - not essential if not literalDictCompare: literalDictCompare = seriesValue else: if isinstance(literalDictCompare, dict) and isinstance(seriesValue, dict): cmp = _compareDict(literalDictCompare, seriesValue) if not cmp: colour = INVALIDROWCOLOUR errorDict = True except Exception as es: # catch exception raised by bad literals colour = INVALIDROWCOLOUR errorState = True finally: if literalTypes and len(literalTypes) > 1: colour = INVALIDROWCOLOUR errorState = True palette.setColor(editor.viewport().backgroundRole(), colour) editor.viewport().setPalette(palette) # rowHeight = QtGui.QFontMetrics(editor.document().defaultFont()).height() # lineCount = editor.document().lineCount() # # minHeight = (rowHeight + 1) * (lineCount + 1) # height = max(editor._minimumHeight, minHeight) # heightSum += height # editor.setMaximumHeight(height) # editor.updateGeometry() self._errorFrameSeriesValues.setVisible(errorState) self._errorFrameDict.setVisible(errorDict) def _changeSpectrumSeriesValues(self, spectrumGroup, spectrum, dim, value): # set the spectrum series value from here # spectrum.seriesValue = value # bit of a hack here - called by _groupInit which builds the spectrumGroup series self._parent._spectrumGroupSeriesEdited[dim] = value @queueStateChange(_verifyPopupApply) def _queueChangeSeriesUnits(self, editor, spectrumGroup): """callback from editing the seriesUnits - respond to every keypress """ value = editor.getText() specValue = spectrumGroup.seriesUnits if value != specValue: return partial(self._changeSeriesUnits, spectrumGroup, value) def _changeSeriesUnits(self, spectrumGroup, value): """set the spectrumGroup seriesUnits """ self._parent._spectrumGroupSeriesUnitsEdited = value def _updateSeriesUnitsPulldown(self, editor, spectrumGroup, newText): """callback from editing the seriesUnits pulldown - respond to index changed/focus changed to capture new text and add to pulldown list """ # NOTE:ED - could put this into the pullDown widget as an 'autoUpdateContents' setting value = newText if value and value not in self._pulldownData: self._pulldownData += (value,) editor.modifyTexts(self._pulldownData) self._queueChangeSeriesUnits(editor, spectrumGroup) def _getChangeState(self): """Get the change state from the parent widget """ return self._parent._getChangeState() @queueStateChange(_verifyPopupApply) def _queueChangeSeriesType(self, spectrumGroup): """callback from editing the seriesType """ self._validateEditors() index = self.seriesType.getIndex() specType = spectrumGroup.seriesType if index != specType: return partial(self._changeSeriesType, spectrumGroup, index) def _changeSeriesType(self, spectrumGroup, value): """set the spectrumGroup seriesType """ self._parent._spectrumGroupSeriesTypeEdited = value @queueStateChange(_verifyPopupApply) def _queueChangeName(self, _value): """callback from editing the name """ editName = self._parent.nameEdit.text() defaultName = self._parent._defaultName if editName != defaultName: return partial(self._changeName, editName) def _changeName(self, value): """set the spectrumGroup seriesType """ # doesn't need to do anything, just insert an item into the revert _changes dict pass @queueStateChange(_verifyPopupApply) def _queueChangeComment(self, _value): """callback from editing the comment """ editComment = self._parent.commentEdit.text() or None defaultComment = self._parent._defaultComment if editComment != defaultComment: return partial(self._changeComment, editComment) def _changeComment(self, value): """set the spectrumGroup seriesType """ # doesn't need to do anything, just insert an item into the revert _changes dict pass @queueStateChange(_verifyPopupApply) def _queueChangeSpectrumList(self): """callback from changing the spectra in the list """ pidList = self._parent._groupedObjects defaultList = self._parent._defaultSpectra if pidList != defaultList: return partial(self._changeSpectrumList, pidList) def _changeSpectrumList(self, value): """set the spectra in the spectrumGroup """ # doesn't need to do anything, just insert an item into the revert _changes dict pass
class _SpectrumGroupContainer(AttrDict): """ Class to simulate a spectrumGroup in popup. """ def __init__(self): super(_SpectrumGroupContainer, self).__init__() self.pid = id(self) self.spectra = [] self._modifiedSpectra = set() self._setDefaultSeriesValues() self._seriesUnits = None self._seriesType = 0 @property def series(self) -> Tuple[Any, ...]: """Returns a tuple of series items for the attached spectra series = (val1, val2, ..., valN) where val1-valN correspond to the series items in the attached spectra associated with this group For a spectrum with no values, returns None in place of Item Duplicated from spectrumGroup.series """ series = () for spectrum in self.spectra: series += (spectrum._getSeriesItemsById(self.pid),) return series @property def seriesUnits(self): """Return the seriesUnits for the simulated spectrumGroup """ return self._seriesUnits @property def seriesType(self): """Return the type for seriesValues widget entry """ return self._seriesType def _setDefaultSeriesValues(self): for spec in self.spectra: spec._setSeriesItemsById(self.pid, None) def _removeDefaultSeriesValues(self): for spec in self.spectra: spec._removeSeriesItemsById(self.pid) def __del__(self): """destructor required to cleanup ids in altered spectra """ self._removeDefaultSeriesValues()