"""This file contains ChemicalShiftTable class
modified by Geerten 1-7/12/2016
tertiary version by Ejb 9/5/17
"""
#=========================================================================================
# 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: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2022-04-05 12:05:16 +0100 (Tue, April 05, 2022) $"
__version__ = "$Revision: 3.1.0 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================
from PyQt5 import QtWidgets, QtCore
from functools import partial
from types import SimpleNamespace
import pandas as pd
from ccpn.core.lib.Notifiers import Notifier
from ccpn.core.lib.DataFrameObject import DataFrameObject
from ccpn.core.ChemicalShiftList import ChemicalShiftList
from ccpn.core.lib.DataFrameObject import DATAFRAME_OBJECT
from ccpn.core.lib.CallBack import CallBack
from ccpn.core.ChemicalShiftList import CS_UNIQUEID, CS_ISDELETED, CS_PID, \
CS_STATIC, CS_STATE, CS_ORPHAN, CS_VALUE, CS_VALUEERROR, CS_FIGUREOFMERIT, CS_ATOMNAME, \
CS_NMRATOM, CS_CHAINCODE, CS_SEQUENCECODE, CS_RESIDUETYPE, \
CS_ALLPEAKS, CS_SHIFTLISTPEAKSCOUNT, CS_ALLPEAKSCOUNT, \
CS_COMMENT, CS_OBJECT, \
CS_TABLECOLUMNS, ChemicalShiftState
from ccpn.core.ChemicalShift import ChemicalShift
from ccpn.util.Logging import getLogger
from ccpn.util.Common import makeIterableList
from ccpn.ui.gui.modules.CcpnModule import CcpnModule
from ccpn.ui.gui.widgets.Widget import Widget
from ccpn.ui.gui.widgets.CompoundWidgets import CheckBoxCompoundWidget
from ccpn.ui.gui.widgets.PulldownListsForObjects import ChemicalShiftListPulldown
from ccpn.ui.gui.widgets.GuiTable import GuiTable, _getValueByHeader
from ccpn.ui.gui.widgets.Column import ColumnClass
from ccpn.ui.gui.widgets.Spacer import Spacer
from ccpn.ui.gui.widgets.MessageDialog import showYesNo, showWarning
from ccpn.ui.gui.widgets.SettingsWidgets import ALL
from ccpn.ui.gui.widgets.Column import COLUMN_COLDEFS, COLUMN_SETEDITVALUE, COLUMN_FORMAT
from ccpn.ui.gui.lib.StripLib import navigateToPositionInStrip
from ccpn.ui.gui.lib._SimplePandasTable import _SimplePandasTableViewProjectSpecific, _updateSimplePandasTable
logger = getLogger()
LINKTOPULLDOWNCLASS = 'linkToPulldownClass'
#=========================================================================================
# ChemicalShiftTableModule
#=========================================================================================
[docs]class ChemicalShiftTableModule(CcpnModule):
"""This class implements the module by wrapping a NmrResidueTable instance
"""
includeSettingsWidget = True
maxSettingsState = 2 # states are defined as: 0: invisible, 1: both visible, 2: only settings visible
settingsPosition = 'left'
className = 'ChemicalShiftTableModule'
_allowRename = True
activePulldownClass = None # e.g., can make the table respond to current peakList
def __init__(self, mainWindow=None, name='Chemical Shift Table',
chemicalShiftList=None, selectFirstItem=False):
"""Initialise the Module widgets
"""
super().__init__(mainWindow=mainWindow, name=name)
# Derive application, project, and current from mainWindow
self.mainWindow = mainWindow
if mainWindow:
self.application = mainWindow.application
self.project = mainWindow.application.project
self.current = mainWindow.application.current
self._table = None
# add the widgets
self._setWidgets()
if chemicalShiftList is not None:
self._selectTable(chemicalShiftList)
elif selectFirstItem:
self._modulePulldown.selectFirstItem()
self.installMaximiseEventHandler(self._maximise, self._closeModule)
def _setWidgets(self):
"""Set up the widgets for the module
"""
# Put all the NmrTable settings in a widget, as there will be more added in the PickAndAssign, and
# backBoneAssignment modules
if self.includeSettingsWidget:
self._CSTwidget = Widget(self.settingsWidget, setLayout=True,
grid=(0, 0), vAlign='top', hAlign='left')
# cannot set a notifier for displays, as these are not (yet?) implemented and the Notifier routines
# underpinning the addNotifier call do not allow for it either
colwidth = 140
self.autoClearMarksWidget = CheckBoxCompoundWidget(
self._CSTwidget,
grid=(3, 0), vAlign='top', stretch=(0, 0), hAlign='left',
fixedWidths=(colwidth, 30),
orientation='left',
labelText='Auto clear marks:',
checked=True
)
_topWidget = self.mainWidget
# main widgets at the top
row = 0
Spacer(_topWidget, 5, 5,
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed,
grid=(0, 0), gridSpan=(1, 1))
row += 1
self._modulePulldown = ChemicalShiftListPulldown(parent=_topWidget,
mainWindow=self.mainWindow, default=None,
grid=(row, 0), gridSpan=(1, 1), minimumWidths=(0, 100),
showSelectName=True,
sizeAdjustPolicy=QtWidgets.QComboBox.AdjustToContents,
callback=self._selectionPulldownCallback,
)
# fixed height
self._modulePulldown.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
row += 1
self.spacer = Spacer(_topWidget, 5, 5,
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed,
grid=(2, 1), gridSpan=(1, 1))
_topWidget.getLayout().setColumnStretch(1, 2)
# main window
_hidden = [CS_UNIQUEID, CS_ISDELETED, CS_FIGUREOFMERIT, CS_ALLPEAKS, CS_CHAINCODE,
CS_SEQUENCECODE, CS_STATE, CS_ORPHAN]
row += 1
self._tableWidget = _NewChemicalShiftTable(parent=_topWidget,
mainWindow=self.mainWindow,
moduleParent=self,
grid=(row, 0), gridSpan=(1, 6),
hiddenColumns=_hidden)
def _maximise(self):
"""Maximise the attached table
"""
self._selectionPulldownCallback(None)
def _selectTable(self, chemicalShiftList=None):
"""Manually select a ChemicalShiftList from the pullDown
"""
if chemicalShiftList is None:
self._modulePulldown.selectFirstItem()
else:
if not isinstance(chemicalShiftList, ChemicalShiftList):
logger.warning('select: Object is not of type ChemicalShiftList')
raise TypeError('select: Object is not of type ChemicalShiftList')
else:
self._modulePulldown.select(chemicalShiftList.pid)
def _closeModule(self):
"""CCPN-INTERNAL: used to close the module
"""
self._modulePulldown.unRegister()
self._tableWidget._close()
super()._closeModule()
def _selectionPulldownCallback(self, item):
"""Notifier Callback for selecting ChemicalShiftList from the pull down menu
"""
self._table = self._modulePulldown.getSelectedObject()
self._tableWidget._table = self._table
if self._table is not None:
self._tableWidget.populateTable(rowObjects=self._table.chemicalShifts,
selectedObjects=self.current.chemicalShifts)
else:
self._tableWidget.populateEmptyTable()
#=========================================================================================
# ChemicalShiftTable
#=========================================================================================
[docs]class ChemicalShiftTable(GuiTable):
"""Class to present a NmrResidue Table and a NmrChain pulldown list, wrapped in a Widget
"""
className = 'ChemicalShiftListTable'
attributeName = 'chemicalShiftLists'
PRIMARYCOLUMN = CS_OBJECT # column holding active objects (uniqueId/ChemicalShift for this table?)
defaultHidden = [CS_UNIQUEID, CS_ISDELETED, CS_FIGUREOFMERIT, CS_ALLPEAKS, CS_CHAINCODE,
CS_SEQUENCECODE, CS_STATE, CS_ORPHAN]
# define self._columns here
columnHeaders = {CS_UNIQUEID : 'Unique ID',
CS_ISDELETED : 'isDeleted', # should never be visible
CS_PID : 'ChemicalShift',
CS_VALUE : 'ChemicalShift Value (ppm)',
CS_VALUEERROR : 'Value Error',
CS_FIGUREOFMERIT : 'Figure of Merit',
CS_NMRATOM : 'NmrAtom',
CS_CHAINCODE : 'ChainCode',
CS_SEQUENCECODE : 'SequenceCode',
CS_RESIDUETYPE : 'ResidueType',
CS_ATOMNAME : 'AtomName',
CS_STATE : 'State',
CS_ORPHAN : 'Orphaned',
CS_ALLPEAKS : 'Assigned Peaks',
CS_SHIFTLISTPEAKSCOUNT: 'Peak Count',
CS_ALLPEAKSCOUNT : 'Total Peak Count',
CS_COMMENT : 'Comment',
CS_OBJECT : '_object'
}
tipTexts = ('Unique identifier for the chemicalShift',
'isDeleted', # should never be visible
'ChemicalShift.pid',
'ChemicalShift value in ppm',
'Error in the chemicalShift value in ppm',
'Figure of merit, between 0 and 1',
'Pid of nmrAtom if attached, or None',
'ChainCode of attached nmrAtom, or None',
'SequenceCode of attached nmrAtom, or None',
'ResidueType of attached nmrAtom, or None',
'AtomName of attached nmrAtom, or None',
'Active state of chemicalShift',
'Orphaned state of chemicalShift',
'List of assigned peaks associated with this chemicalShift',
'Number of assigned peaks attached to a chemicalShift\nbelonging to spectra associated with parent chemicalShiftList',
'Total number of assigned peaks attached to a chemicalShift\nbelonging to any spectra',
'Optional comment for each chemicalShift',
'None',
)
def __init__(self, parent=None, mainWindow=None, moduleParent=None,
actionCallback=None, selectionCallback=None,
chemicalShiftList=None, hiddenColumns=None, **kwds):
"""Initialise the widgets for the module.
"""
# Derive application, project, and current from mainWindow
self.mainWindow = mainWindow
if mainWindow:
self.application = mainWindow.application
self.project = mainWindow.application.project
self.current = mainWindow.application.current
self._table = None
# self._widget = Widget(parent=parent, **kwds)
# self._selectedChemicalShiftList = None
# Initialise the scroll widget and common settings
self._initTableCommonWidgets(parent, **kwds)
# initialise the currently attached dataFrame
self._hiddenColumns = [self.columnHeaders[col] for col in hiddenColumns] if hiddenColumns else \
[self.columnHeaders[col] for col in self.defaultHidden]
self.dataFrameObject = None
selectionCallback = self._selectionCallback if selectionCallback is None else selectionCallback
actionCallback = self._actionCallback if actionCallback is None else actionCallback
# create the table; objects are added later via the displayTableForNmrChain method
# initialise the table
super().__init__(parent=parent,
mainWindow=self.mainWindow,
setLayout=True,
multiSelect=True,
actionCallback=actionCallback,
selectionCallback=selectionCallback,
grid=(3, 0), gridSpan=(1, 6),
)
self.moduleParent = moduleParent
self.setTableNotifiers(tableClass=ChemicalShiftList,
rowClass=ChemicalShift,
# cellClassNames=(NmrAtom, '_chemicalShifts'), # not required
tableName='chemicalShiftList', rowName='chemicalShift',
changeFunc=self._tableChangeNotifierCallback,
className=self.attributeName,
# updateFunc=self._update,
tableSelection='_table',
callBackClass=ChemicalShift,
selectCurrentCallBack=self._selectOnTableCurrentChemicalShiftNotifierCallback,
moduleParent=moduleParent)
# Initialise the notifier for processing dropped items
self._postInitTableCommonWidgets()
def _processDroppedItems(self, data):
"""CallBack for Drop events
"""
pids = data.get('pids', [])
self._handleDroppedItems(pids, ChemicalShiftList, self.moduleParent._modulePulldown)
def _getValidChemicalShift4Callback(self, objs):
if not objs or not all(objs):
return
if isinstance(objs, (tuple, list)):
cShift = objs[-1]
else:
cShift = objs
if not cShift:
showWarning('Cannot perform action', 'No selected ChemicalShift')
return
return cShift
def _tableChangeNotifierCallback(self, table):
"""Respond to table has been changed, e.g. renamed
"""
if self._table is not None:
self.populateTable(rowObjects=self._table.chemicalShifts,
selectedObjects=self.current.chemicalShifts)
else:
self.clearTable()
#=========================================================================================
# Widgets callbacks
#=========================================================================================
def _actionCallback(self, data):
"""Notifier DoubleClick action on item in table. Mark a chemicalShift based on attached nmrAtom
"""
from ccpn.AnalysisAssign.modules.BackboneAssignmentModule import markNmrAtoms
cShift = self._getValidChemicalShift4Callback(data.get(CallBack.OBJECT, []))
if len(self.mainWindow.marks):
if self.moduleParent.autoClearMarksWidget.checkBox.isChecked():
self.mainWindow.clearMarks()
if cShift and cShift.nmrAtom:
markNmrAtoms(self.mainWindow, [cShift.nmrAtom])
def _selectionCallback(self, data):
"""Notifier Callback for selecting rows in the table
"""
objs = data[CallBack.OBJECT]
self.current.chemicalShifts = objs or []
if objs:
nmrResidues = tuple(set(cs.nmrAtom.nmrResidue for cs in objs if cs.nmrAtom))
else:
nmrResidues = []
if nmrResidues:
# set the associated nmrResidue and nmrAtoms
nmrAtoms = tuple(set(nmrAtom for nmrRes in nmrResidues for nmrAtom in nmrRes.nmrAtoms))
self.current.nmrAtoms = nmrAtoms
self.current.nmrResidues = nmrResidues
else:
self.current.nmrAtoms = []
self.current.nmrResidues = []
#=========================================================================================
# Menus
#=========================================================================================
def _setContextMenu(self, enableExport=True, enableDelete=True):
"""Subclass guiTable to insert new merge items to top of context menu
"""
super()._setContextMenu(enableExport=enableExport, enableDelete=enableDelete)
_actions = self.tableMenu.actions()
if _actions:
_topMenuItem = _actions[0]
_topSeparator = self.tableMenu.insertSeparator(_topMenuItem)
self._navigateMenu = self.tableMenu.addMenu('Navigate to:')
self._mergeMenuAction = self.tableMenu.addAction('Merge NmrAtoms', self._mergeNmrAtoms)
self._editMenuAction = self.tableMenu.addAction('Edit NmrAtom', self._editNmrAtom)
# move new actions to the top of the list
self.tableMenu.insertAction(_topSeparator, self._mergeMenuAction)
self.tableMenu.insertAction(self._mergeMenuAction, self._editMenuAction)
def _addNavigationStripsToContextMenu(self):
cShift = self._getValidChemicalShift4Callback(self.getSelectedObjects())
self._navigateMenu.clear()
if cShift and cShift.nmrAtom:
name = cShift.nmrAtom.name
if cShift.value is None:
return
value = round(cShift.value, 3)
if self._navigateMenu is not None:
self._navigateMenu.addItem(f'All ({name}:{value})',
callback=partial(self._navigateToChemicalShift,
chemicalShift=cShift,
stripPid=ALL))
self._navigateMenu.addSeparator()
for spectrumDisplay in self.mainWindow.spectrumDisplays:
for strip in spectrumDisplay.strips:
self._navigateMenu.addItem(f'{strip.pid} ({name}:{value})',
callback=partial(self._navigateToChemicalShift,
chemicalShift=cShift,
stripPid=strip.pid))
self._navigateMenu.addSeparator()
def _navigateToChemicalShift(self, chemicalShift, stripPid):
# TODO add check if chemicalShift.spectra are in the strip.
strips = []
if stripPid == ALL:
strips = self.mainWindow.strips
else:
strip = self.application.getByGid(stripPid)
if strip:
strips.append(strip)
if strips:
failedStripPids = []
for strip in strips:
try:
navigateToPositionInStrip(strip,
positions=[chemicalShift.value],
axisCodes=[chemicalShift.nmrAtom.name],
widths=[])
except:
failedStripPids.append(strip.pid)
if len(failedStripPids) > 0:
stripStr = 'strip' if len(failedStripPids) == 1 else 'strips'
strips = ', '.join(failedStripPids)
getLogger().warn(
f'Cannot navigate to position {round(chemicalShift.value, 3)} '
f'in {stripStr}: {strips} '
f'for nmrAtom {chemicalShift.nmrAtom.name}.')
def _raiseTableContextMenu(self, pos):
"""Create a new menu and popup at cursor position
Add merge item
"""
selection = self.getSelectedObjects()
data = self.getRightMouseItem()
if data:
cShift = data.get(DATAFRAME_OBJECT)
currentNmrAtom = cShift.nmrAtom if cShift else None
selection = [ch.nmrAtom for ch in selection or [] if ch.nmrAtom]
_check = (currentNmrAtom and 1 < len(selection) and currentNmrAtom in selection) or False
_option = 'into {}'.format(currentNmrAtom.id if currentNmrAtom else '') if _check else ''
self._mergeMenuAction.setText('Merge NmrAtoms {}'.format(_option))
self._mergeMenuAction.setEnabled(_check)
self._editMenuAction.setText('Edit NmrAtom {}'.format(currentNmrAtom.id if currentNmrAtom else ''))
self._editMenuAction.setEnabled(True if currentNmrAtom else False)
self._addNavigationStripsToContextMenu()
else:
# disabled but visible lets user know that menu items exist
self._mergeMenuAction.setText('Merge NmrAtoms')
self._mergeMenuAction.setEnabled(False)
self._editMenuAction.setText('Edit NmrAtom')
self._editMenuAction.setEnabled(False)
super()._raiseTableContextMenu(pos)
def _mergeNmrAtoms(self):
"""Merge the nmrAtoms in the selection into the nmrAtom that has ben right-clicked
"""
selection = self.getSelectedObjects()
data = self.getRightMouseItem()
if data and selection:
cShift = data.get(DATAFRAME_OBJECT)
currentNmrAtom = cShift.nmrAtom if cShift else None
matching = [ch.nmrAtom for ch in selection if ch and ch.nmrAtom and ch.nmrAtom != currentNmrAtom and
ch.nmrAtom.isotopeCode == currentNmrAtom.isotopeCode]
nonMatching = [ch.nmrAtom for ch in selection if ch and ch.nmrAtom and ch.nmrAtom != currentNmrAtom and
ch.nmrAtom.isotopeCode != currentNmrAtom.isotopeCode]
if len(matching) < 1:
showWarning('Merge NmrAtoms', 'No matching isotope codes')
else:
ss = 's' if (len(nonMatching) > 1) else ''
nonMatchingList = '\n\n\n({} nmrAtom{} with non-matching isotopeCode{})'.format(len(nonMatching), ss, ss) if nonMatching else ''
yesNo = showYesNo('Merge NmrAtoms', "Do you want to merge\n\n"
"{} into {}{}".format('\n'.join([ss.id for ss in matching]),
currentNmrAtom.id,
nonMatchingList))
if yesNo:
currentNmrAtom.mergeNmrAtoms(matching)
def _editNmrAtom(self):
"""Show the edit nmrAtom popup for the clicked nmrAtom
"""
data = self.getRightMouseItem()
if data:
cShift = data.get(DATAFRAME_OBJECT)
currentNmrAtom = cShift.nmrAtom if cShift else None
if currentNmrAtom:
from ccpn.ui.gui.popups.NmrAtomPopup import NmrAtomEditPopup
popup = NmrAtomEditPopup(parent=self.mainWindow, mainWindow=self.mainWindow, obj=currentNmrAtom)
popup.exec_()
def _selectOnTableCurrentChemicalShiftNotifierCallback(self, data):
"""Callback from a notifier to highlight the chemical shifts
:param data:
"""
currentShifts = data['value']
self._selectOnTableCurrentChemicalShifts(currentShifts)
def _selectOnTableCurrentChemicalShifts(self, currentShifts):
"""Highlight the list of currentShifts on the table
:param currentShifts:
"""
self.highlightObjects(currentShifts)
@staticmethod
def _getShiftPeakCount(chemicalShift):
"""CCPN-INTERNAL: Return number of peaks assigned to NmrAtom in Experiments and PeakLists
using ChemicalShiftList
"""
chemicalShiftList = chemicalShift.chemicalShiftList
peaks = chemicalShift.nmrAtom.assignedPeaks
return (len(set(x for x in peaks
if x.peakList.chemicalShiftList is chemicalShiftList)))
# def getCellToRows(self, cellItem, attribute):
# """Get the list of objects which cellItem maps to for this table
# To be subclassed as required
# """
# # classItem is usually a type such as PeakList, MultipletList
# # with an attribute such as peaks/peaks
#
# # this is a step towards making guiTableABC and subclass for each table
# return getattr(cellItem, attribute, []), None
@staticmethod
def _stLamFloat(row, name):
"""CCPN-INTERNAL: used to display Table
"""
try:
return float(getattr(row, name))
except:
return None
@staticmethod
def _getNmrChain(chemicalShift):
"""CCPN-INTERNAL: get the nmrChain for the nmrResidue associated with this chemicalShift
"""
try:
return chemicalShift.nmrAtom.nmrResidue.nmrChain.id
except:
return None
@staticmethod
def _getSequenceCode(chemicalShift):
"""CCPN-INTERNAL: get the sequenceCode for the nmrResidue associated with this chemicalShift
"""
try:
return chemicalShift.nmrAtom.nmrResidue.sequenceCode
except:
return None
@staticmethod
def _getResidueType(chemicalShift):
"""CCPN-INTERNAL: get the residueType for the nmrResidue associated with this chemicalShift
"""
try:
return chemicalShift.nmrAtom.nmrResidue.residueType
except:
return None
@staticmethod
def _getNmrResidue(chemicalShift):
"""CCPN-INTERNAL: get the nmrResidue for the nmrResidue associated with this chemicalShift
"""
try:
return chemicalShift.nmrAtom.nmrResidue
except:
return None
#=========================================================================================
# Subclass GuiTable
#=========================================================================================
[docs] def populateTable(self, rowObjects=None, columnDefs=None,
selectedObjects=None):
"""Populate the table with a set of objects to highlight, or keep current selection highlighted
with the first item visible.
Use selectedObjects = [] to clear the selected items
:param rowObjects: list of objects to set each row
"""
self.project.blankNotification()
# if nothing passed in then keep the current highlighted objects
objs = selectedObjects if selectedObjects is not None else self.getSelectedObjects()
try:
_dataFrameObject = self.getDataFrameFromExpandedList(table=self,
buildList=rowObjects,
expandColumn='Restraint')
# populate from the Pandas dataFrame inside the dataFrameObject
self.setTableFromDataFrameObject(dataFrameObject=_dataFrameObject, columnDefs=self._columns)
except Exception as es:
getLogger().warning('Error populating table', str(es))
# raise es
finally:
self._highLightObjs(objs)
self.project.unblankNotification()
def _highLightObjs(self, selection, scrollToSelection=True):
# skip if the table is empty
if not self._dataFrameObject:
return
with self._tableBlockSignals('_highLightObjs'):
selectionModel = self.selectionModel()
model = self.model()
itm = self.currentItem()
selectionModel.clearSelection()
if selection:
if len(selection) > 0:
if isinstance(selection[0], pd.Series):
# not sure how to handle this
return
uniqObjs = set(selection)
# NOTE:ED - fix this?
# _shiftObjects = tuple(_getValueByHeader(row, CS_OBJECT) for row in self._dataFrameObject.objects)
rows = [self._dataFrameObject.find(self, str(obj), column=CS_OBJECT, multiRow=True) for obj in uniqObjs]
# if obj in _peakObjects and obj.peakList == self._selectedPeakList]
rows = [row for row in set(makeIterableList(rows)) if row is not None]
if rows:
rows.sort(key=lambda c: int(c))
# remember the current cell so that cursor work correctly
if itm and itm.row() in rows:
self.setCurrentItem(itm)
_row = itm.row()
else:
_row = rows[0]
rowIndex = model.index(_row, 0)
self.setCurrentIndex(rowIndex)
for row in rows:
if row != _row:
rowIndex = model.index(row, 0)
selectionModel.select(rowIndex, selectionModel.Select | selectionModel.Rows)
if scrollToSelection and not self._scrollOverride:
self.scrollToSelectedIndex()
def _derivedFromObject(self, obj):
"""Get a tuple of derived values from obj
Not very generic yet - column class now seems redundant
"""
_peaks = obj.assignedPeaks or []
allPeaks = str([pp.pid for pp in _peaks])
try:
shiftPeakCount = len([pp for pp in _peaks if pp.spectrum.chemicalShiftList == obj.chemicalShiftList])
except Exception as es:
shiftPeakCount = 0
peakCount = len(_peaks) if _peaks else 0
state = obj.state
if state == ChemicalShiftState.ORPHAN:
state = ChemicalShiftState.DYNAMIC
state = state.description # if state needed
orphan = u'\u2713' if obj.orphan else '' # unicode tick character
return (state, orphan, allPeaks, shiftPeakCount, peakCount)
[docs] def getDataFrameFromExpandedList(self, table=None,
buildList=None,
colDefs=None,
expandColumn=None):
"""
Return a Pandas dataFrame from an internal list of objects
The columns are based on the 'func' functions in the columnDefinitions
:param buildList:
:param colDefs:
:return pandas dataFrameObject:
"""
# create the column objects
_cols = [
# (col, lambda row: _getValueByHeader(row, col), _tipTexts[ii], None, None)
(self.columnHeaders[col], lambda row: _getValueByHeader(row, col), self.tipTexts[ii], None, None)
for ii, col in enumerate(CS_TABLECOLUMNS)
]
# NOTE:ED - hack to add the comment editor to the comment column, decimal places to value/valueError/figureOfMerit
_temp = list(_cols[CS_TABLECOLUMNS.index(CS_COMMENT)])
_temp[COLUMN_COLDEFS.index(COLUMN_SETEDITVALUE)] = lambda obj, value: self._setComment(obj, value)
_cols[CS_TABLECOLUMNS.index(CS_COMMENT)] = tuple(_temp)
for col in [CS_VALUE, CS_VALUEERROR, CS_FIGUREOFMERIT]:
_temp = list(_cols[CS_TABLECOLUMNS.index(col)])
_temp[COLUMN_COLDEFS.index(COLUMN_FORMAT)] = '%0.3f'
_cols[CS_TABLECOLUMNS.index(col)] = tuple(_temp)
# set the table _columns
self._columns = ColumnClass(_cols)
_csl = self._table
if _csl._data is not None:
# is of type _ChemicalShiftListFrame - should move functionality to there
_table = _csl._data.copy()
_table = _table[_table[CS_ISDELETED] == False]
_table.drop(columns=[CS_STATIC], inplace=True) # static not required
_table.set_index(_table[CS_UNIQUEID], inplace=True, ) # drop=False)
_table.insert(CS_TABLECOLUMNS.index(CS_PID), CS_PID, None)
_table.insert(CS_TABLECOLUMNS.index(CS_STATE), CS_STATE, None) # if state require
_table.insert(CS_TABLECOLUMNS.index(CS_ORPHAN), CS_ORPHAN, None) # if state require
_table.insert(CS_TABLECOLUMNS.index(CS_ALLPEAKS), CS_ALLPEAKS, None)
_table.insert(CS_TABLECOLUMNS.index(CS_SHIFTLISTPEAKSCOUNT), CS_SHIFTLISTPEAKSCOUNT, None)
_table.insert(CS_TABLECOLUMNS.index(CS_ALLPEAKSCOUNT), CS_ALLPEAKSCOUNT, None)
_objs = [_csl.getChemicalShift(uniqueId=unq) for unq in _table[CS_UNIQUEID]]
if _objs:
# append the actual objects as the last column - not sure whether this is required - check _highlightObjs
_table[CS_OBJECT] = _objs
_table[CS_PID] = [_shift.pid for _shift in _objs]
_stats = [self._derivedFromObject(obj) for obj in _objs]
# _table[[CS_ALLPEAKS, CS_SHIFTLISTPEAKSCOUNT, CS_ALLPEAKSCOUNT]] = _stats
_table[[CS_STATE, CS_ORPHAN, CS_ALLPEAKS, CS_SHIFTLISTPEAKSCOUNT, CS_ALLPEAKSCOUNT]] = _stats
# replace the visible nans with '' for comment column and string 'None' elsewhere
_table[CS_COMMENT].fillna('', inplace=True)
_table.fillna('None', inplace=True)
else:
# _table = pd.DataFrame(columns=CS_TABLECOLUMNS)
_table = pd.DataFrame(columns=[self.columnHeaders[val] for val in CS_TABLECOLUMNS])
# set the table from the dataFrame
_dataFrame = DataFrameObject(dataFrame=_table,
columnDefs=self._columns or [],
table=table,
)
# extract the row objects from the dataFrame
_objects = [row for row in _table.itertuples()]
_dataFrame._objects = _objects
return _dataFrame
[docs] def refreshTable(self):
# subclass to refresh the groups
self.setTableFromDataFrameObject(self._dataFrameObject)
# self.updateTableExpanders()
# self._updateGroups(dataFrame)
# self.updateTableExpanders()
def _newRowFromUniqueId(self, df, obj, uniqueId):
# NOTE:ED - this needs to go elsewhere?
# need to define a row handler rather than a column handler
_row = df.loc[uniqueId]
# make the new row
newRow = _row[:CS_ISDELETED].copy()
_midRow = _row[CS_VALUE:CS_ATOMNAME] # CS_STATIC
_comment = _row[CS_COMMENT:]
_pidCol = pd.Series(obj.pid, index=[CS_PID, ])
_extraCols = pd.Series(self._derivedFromObject(obj), index=[CS_STATE, CS_ORPHAN, CS_ALLPEAKS, CS_SHIFTLISTPEAKSCOUNT, CS_ALLPEAKSCOUNT]) # if state required
newRow = newRow.append([_pidCol, _midRow, _extraCols, _comment])
# append the actual object to the end - not sure whether this is required - check _highlightObjs
newRow[CS_OBJECT] = obj
# replace the visible nans with '' for comment column and string 'None' elsewhere
newRow[CS_COMMENT:CS_COMMENT].fillna('', inplace=True)
newRow.fillna('None', inplace=True)
return list(newRow)
def _updateRowCallback(self, data):
"""
Notifier callback for updating the table for change in nmrRows
:param data:
"""
with self._tableBlockSignals('_updateRowCallback'):
obj = data[Notifier.OBJECT]
uniqueId = obj.uniqueId
# check that the object belongs to the list that is being displayed
if not self._dataFrameObject or obj is None:
return
if obj.chemicalShiftList != self._table:
return
_update = False # from original row update - need to check
trigger = data[Notifier.TRIGGER]
try:
_df = self._table._data
_df = _df[_df[CS_ISDELETED] == False] # not deleted - should be the only visible ones
# the column containing the uniqueId
col = CS_TABLECOLUMNS.index(CS_UNIQUEID)
tableIds = tuple(self.item(rr, col).value for rr in range(self.rowCount()))
if trigger == Notifier.DELETE:
# uniqueIds in the visible table
if uniqueId in (set(tableIds) - set(_df[CS_UNIQUEID])):
# remove from the table
self._dataFrameObject._dataFrame.drop([uniqueId], inplace=True)
self.removeRow(tableIds.index(uniqueId))
elif trigger == Notifier.CREATE:
# uniqueIds in the visible table
if uniqueId in (set(_df[CS_UNIQUEID]) - set(tableIds)):
newRow = self._newRowFromUniqueId(_df, obj, uniqueId)
# visible table dataframe update
self._dataFrameObject._dataFrame.loc[uniqueId] = newRow
# update the table widgets - really need to change to QTableView (think it was actually this before)
self.addRow(newRow)
elif trigger == Notifier.CHANGE:
# uniqueIds in the visible table
if uniqueId in (set(_df[CS_UNIQUEID]) & set(tableIds)):
newRow = self._newRowFromUniqueId(_df, obj, uniqueId)
# visible table dataframe update
self._dataFrameObject._dataFrame.loc[uniqueId] = newRow
# update the table widgets - really need to change to QTableView (think it was actually this before)
self.setRow(tableIds.index(uniqueId), newRow)
elif trigger == Notifier.RENAME:
# not sure that I need this yet
pass
except Exception as es:
getLogger().debug2(f'Error updating row in table {es}')
if _update:
getLogger().debug2('<updateRowCallback>', data['notifier'],
self._tableData['tableSelection'],
data['trigger'], data['object'])
_val = self.getSelectedObjects() or []
self._tableSelectionChanged.emit(_val)
return _update
[docs] def clearSelection(self):
"""Clear the current selection in the table
and remove objects from the current list
"""
with self._tableBlockSignals('clearSelection'):
# get the selected objects from the table
objList = self.getSelectedObjects()
self.selectionModel().clearSelection()
# remove from the current list
multiple = self._tableData['classCallBack']
if self._dataFrameObject and multiple:
multipleAttr = getattr(self.current, multiple, [])
if len(multipleAttr) > 0:
# need to remove objList from multipleAttr - fires only one current change
setattr(self.current, multiple, tuple(set(multipleAttr) - set(objList)))
self._lastSelection = [None]
self._tableSelectionChanged.emit([])
#=========================================================================================
# New ChemicalShiftTable
#=========================================================================================
# define a simple class that can contains a simple id
blankId = SimpleNamespace(className='notDefined', serial=0)
OBJECT_CLASS = 0
OBJECT_PARENT = 1
MODULEIDS = {}
class _NewChemicalShiftTable(_SimplePandasTableViewProjectSpecific):
"""New chemicalShiftTable based on faster QTableView
Actually more like the original table but with pandas dataFrame
"""
className = 'ChemicalShiftListTable'
attributeName = 'chemicalShiftLists'
PRIMARYCOLUMN = CS_OBJECT # column holding active objects (uniqueId/ChemicalShift for this table?)
defaultHidden = [CS_UNIQUEID, CS_ISDELETED, CS_FIGUREOFMERIT, CS_ALLPEAKS, CS_CHAINCODE,
CS_SEQUENCECODE, CS_STATE, CS_ORPHAN]
_internalColumns = [CS_ISDELETED, CS_OBJECT] # columns that are always hidden
# define self._columns here
columnHeaders = {CS_UNIQUEID : 'Unique ID',
CS_ISDELETED : 'isDeleted', # should never be visible
CS_PID : 'ChemicalShift',
CS_VALUE : 'ChemicalShift Value (ppm)',
CS_VALUEERROR : 'Value Error',
CS_FIGUREOFMERIT : 'Figure of Merit',
CS_NMRATOM : 'NmrAtom',
CS_CHAINCODE : 'ChainCode',
CS_SEQUENCECODE : 'SequenceCode',
CS_RESIDUETYPE : 'ResidueType',
CS_ATOMNAME : 'AtomName',
CS_STATE : 'State',
CS_ORPHAN : 'Orphaned',
CS_ALLPEAKS : 'Assigned Peaks',
CS_SHIFTLISTPEAKSCOUNT: 'Peak Count',
CS_ALLPEAKSCOUNT : 'Total Peak Count',
CS_COMMENT : 'Comment',
CS_OBJECT : '_object'
}
tipTexts = ('Unique identifier for the chemicalShift',
'isDeleted', # should never be visible
'ChemicalShift.pid',
'ChemicalShift value in ppm',
'Error in the chemicalShift value in ppm',
'Figure of merit, between 0 and 1',
'Pid of nmrAtom if attached, or None',
'ChainCode of attached nmrAtom, or None',
'SequenceCode of attached nmrAtom, or None',
'ResidueType of attached nmrAtom, or None',
'AtomName of attached nmrAtom, or None',
'Active state of chemicalShift',
'Orphaned state of chemicalShift',
'List of assigned peaks associated with this chemicalShift',
'Number of assigned peaks attached to a chemicalShift\nbelonging to spectra associated with parent chemicalShiftList',
'Total number of assigned peaks attached to a chemicalShift\nbelonging to any spectra',
'Optional comment for each chemicalShift',
'None',
)
# define the notifiers that are required for the specific table-type
tableClass = ChemicalShiftList
rowClass = ChemicalShift
cellClass = None
tableName = tableClass.className
rowName = tableClass.className
cellName = None
cellClassNames = None
selectCurrent = True
callBackClass = ChemicalShift
search = False
def __init__(self, parent=None, mainWindow=None, moduleParent=None,
actionCallback=None, selectionCallback=None,
chemicalShiftList=None, hiddenColumns=None,
enableExport=True, enableDelete=True, enableSearch=False,
**kwds):
"""Initialise the widgets for the module.
"""
# initialise the currently attached dataFrame
self._hiddenColumns = [self.columnHeaders[col] for col in hiddenColumns] if hiddenColumns else \
[self.columnHeaders[col] for col in self.defaultHidden]
self._internalColumns = [self.columnHeaders[col] for col in self._internalColumns]
# create the table; objects are added later via the displayTableForNmrChain method
# initialise the table
super().__init__(parent=parent,
mainWindow=mainWindow,
moduleParent=moduleParent,
multiSelect=True,
showVerticalHeader=False,
setLayout=True,
**kwds
)
# Initialise the notifier for processing dropped items
self._postInitTableCommonWidgets()
# set the delegate for editing
delegate = _CSLTableDelegate(self)
self.setItemDelegate(delegate)
def _postInitTableCommonWidgets(self):
from ccpn.ui.gui.widgets.DropBase import DropBase
from ccpn.ui.gui.lib.GuiNotifier import GuiNotifier
from ccpn.ui.gui.widgets.ScrollBarVisibilityWatcher import ScrollBarVisibilityWatcher
# add a dropped notifier to all tables
if self.moduleParent is not None:
# set the dropEvent to the mainWidget of the module, otherwise the event gets stolen by Frames
self.moduleParent.mainWidget._dropEventCallback = self._processDroppedItems
self.droppedNotifier = GuiNotifier(self,
[GuiNotifier.DROPEVENT], [DropBase.PIDS],
self._processDroppedItems)
# add a widget handler to give a clean corner widget for the scroll area
self._cornerDisplay = ScrollBarVisibilityWatcher(self)
#=========================================================================================
# Widgets callbacks
#=========================================================================================
def _getValidChemicalShift4Callback(self, objs):
if not objs or not all(objs):
return
if isinstance(objs, (tuple, list)):
cShift = objs[-1]
else:
cShift = objs
if not cShift:
showWarning('Cannot perform action', 'No selected ChemicalShift')
return
return cShift
def actionCallback(self, data):
"""Notifier DoubleClick action on item in table. Mark a chemicalShift based on attached nmrAtom
"""
from ccpn.AnalysisAssign.modules.BackboneAssignmentModule import markNmrAtoms
cShift = self._getValidChemicalShift4Callback(data.get(CallBack.OBJECT, []))
if len(self.mainWindow.marks):
if self.moduleParent.autoClearMarksWidget.checkBox.isChecked():
self.mainWindow.clearMarks()
if cShift and cShift.nmrAtom:
markNmrAtoms(self.mainWindow, [cShift.nmrAtom])
def selectionCallback(self, data):
"""Notifier Callback for selecting rows in the table
"""
objs = data[CallBack.OBJECT]
self.current.chemicalShifts = objs or []
if objs:
nmrResidues = tuple(set(cs.nmrAtom.nmrResidue for cs in objs if cs.nmrAtom))
else:
nmrResidues = []
if nmrResidues:
# set the associated nmrResidue and nmrAtoms
nmrAtoms = tuple(set(nmrAtom for nmrRes in nmrResidues for nmrAtom in nmrRes.nmrAtoms))
self.current.nmrAtoms = nmrAtoms
self.current.nmrResidues = nmrResidues
else:
self.current.nmrAtoms = []
self.current.nmrResidues = []
#=========================================================================================
# Create table and row methods
#=========================================================================================
def _newRowFromUniqueId(self, df, obj, uniqueId):
"""Create a new row to insert into the dataFrame or replace row
"""
_row = df.loc[uniqueId]
# make the new row
newRow = _row[:CS_ISDELETED].copy()
_midRow = _row[CS_VALUE:CS_ATOMNAME] # CS_STATIC
_comment = _row[CS_COMMENT:]
_pidCol = pd.Series(obj.pid, index=[CS_PID, ])
_extraCols = pd.Series(self._derivedFromObject(obj), index=[CS_STATE, CS_ORPHAN, CS_ALLPEAKS, CS_SHIFTLISTPEAKSCOUNT, CS_ALLPEAKSCOUNT]) # if state required
newRow = newRow.append([_pidCol, _midRow, _extraCols, _comment])
# append the actual object to the end - not sure whether this is required - check _highlightObjs
newRow[CS_OBJECT] = obj
# replace the visible nans with '' for comment column and string 'None' elsewhere
newRow[CS_COMMENT:CS_COMMENT].fillna('', inplace=True)
newRow.fillna('None', inplace=True)
return list(newRow)
def _derivedFromObject(self, obj):
"""Get a tuple of derived values from obj
Not very generic yet - column class now seems redundant
"""
_allPeaks = obj.allAssignedPeaks
totalPeakCount = len(_allPeaks)
peaks = [pp.pid for pp in _allPeaks if pp.spectrum.chemicalShiftList == obj.chemicalShiftList]
peakCount = len(peaks)
state = obj.state
if state == ChemicalShiftState.ORPHAN:
state = ChemicalShiftState.DYNAMIC
state = state.description # if state needed
orphan = u'\u2713' if obj.orphan else '' # unicode tick character
return (state, orphan, str(peaks), peakCount, totalPeakCount)
@staticmethod
def _setComment(obj, value):
"""CCPN-INTERNAL: Insert a comment into object
"""
obj.comment = value if value else None
def buildTableDataFrame(self):
"""Return a Pandas dataFrame from an internal list of objects.
The columns are based on the 'func' functions in the columnDefinitions.
:return pandas dataFrame
"""
# create the column objects
_cols = [
# (col, lambda row: _getValueByHeader(row, col), _tipTexts[ii], None, None)
(self.columnHeaders[col], lambda row: _getValueByHeader(row, col), self.tipTexts[ii], None, None)
for ii, col in enumerate(CS_TABLECOLUMNS)
]
# NOTE:ED - hack to add the comment editor to the comment column, decimal places to value/valueError/figureOfMerit
_temp = list(_cols[CS_TABLECOLUMNS.index(CS_COMMENT)])
_temp[COLUMN_COLDEFS.index(COLUMN_SETEDITVALUE)] = lambda obj, value: self._setComment(obj, value)
_cols[CS_TABLECOLUMNS.index(CS_COMMENT)] = tuple(_temp)
for col in [CS_VALUE, CS_VALUEERROR, CS_FIGUREOFMERIT]:
_temp = list(_cols[CS_TABLECOLUMNS.index(col)])
_temp[COLUMN_COLDEFS.index(COLUMN_FORMAT)] = '%0.3f'
_cols[CS_TABLECOLUMNS.index(col)] = tuple(_temp)
# set the table _columns
self._columns = ColumnClass(_cols)
_csl = self._table
if _csl._data is not None:
# is of type _ChemicalShiftListFrame - should move functionality to there
df = _csl._data.copy()
df = df[df[CS_ISDELETED] == False]
df.drop(columns=[CS_STATIC], inplace=True) # static not required
df.set_index(df[CS_UNIQUEID], inplace=True, ) # drop=False)
df.insert(CS_TABLECOLUMNS.index(CS_PID), CS_PID, None)
df.insert(CS_TABLECOLUMNS.index(CS_STATE), CS_STATE, None) # if state require
df.insert(CS_TABLECOLUMNS.index(CS_ORPHAN), CS_ORPHAN, None) # if state require
df.insert(CS_TABLECOLUMNS.index(CS_ALLPEAKS), CS_ALLPEAKS, None)
df.insert(CS_TABLECOLUMNS.index(CS_SHIFTLISTPEAKSCOUNT), CS_SHIFTLISTPEAKSCOUNT, None)
df.insert(CS_TABLECOLUMNS.index(CS_ALLPEAKSCOUNT), CS_ALLPEAKSCOUNT, None)
_objs = _csl._shifts
if _objs:
# append the actual objects as the last column - not sure whether this is required - check _highlightObjs
df[CS_OBJECT] = _objs
df[CS_PID] = [_shift.pid for _shift in _objs]
_stats = [self._derivedFromObject(obj) for obj in _objs]
df[[CS_STATE, CS_ORPHAN, CS_ALLPEAKS, CS_SHIFTLISTPEAKSCOUNT, CS_ALLPEAKSCOUNT]] = _stats
# replace the visible nans with '' for comment column and string 'None' elsewhere
df[CS_COMMENT].fillna('', inplace=True)
df.fillna('None', inplace=True)
else:
df[CS_OBJECT] = []
else:
df = pd.DataFrame(columns=[self.columnHeaders[val] for val in CS_TABLECOLUMNS])
# extract the row objects from the dataFrame
_objects = [row for row in df.itertuples()]
self._objects = _objects
# update the columns to the visible headings
df.columns = [self.columnHeaders[val] for val in CS_TABLECOLUMNS]
# set the table from the dataFrame
_dfObject = DataFrameObject(dataFrame=df,
columnDefs=self._columns or [],
table=self,
)
_dfObject._objects = _objects
return _dfObject
def refreshTable(self):
# subclass to refresh the groups
_updateSimplePandasTable(self, self._df)
# self.updateTableExpanders()
def setDataFromSearchWidget(self, dataFrame):
"""Set the data for the table from the search widget
"""
_updateSimplePandasTable(self, dataFrame)
# self._updateGroups(dataFrame)
# self.updateTableExpanders()
def _updateTableCallback(self, data):
# print(f'>>> _updateTableCallback')
pass
def _updateCellCallback(self, data):
# print(f'>>> _updateCellCallback')
pass
def _searchCallBack(self, data):
# print(f'>>> _searchCallBack')
pass
def _updateRowCallback(self, data):
"""Notifier callback for updating the table for change in chemicalShifts
:param data: notifier content
"""
with self._blockTableSignals('_updateRowCallback'):
obj = data[Notifier.OBJECT]
uniqueId = obj.uniqueId
# check that the object belongs to the list that is being displayed
if self._df is None or self._df.empty or obj is None:
return
if obj.chemicalShiftList != self._table:
return
_update = False # from original row update - need to check
trigger = data[Notifier.TRIGGER]
try:
_data = self._table._data
_data = _data[_data[CS_ISDELETED] == False] # not deleted - should be the only visible ones
dataIds = set(_data[CS_UNIQUEID])
tableIds = set(self._df['Unique ID']) # must be table column name, not reference name
if trigger == Notifier.DELETE:
# uniqueIds in the visible table
if uniqueId in (tableIds - dataIds):
# remove from the table
self.model()._deleteRow(uniqueId)
elif trigger == Notifier.CREATE:
# uniqueIds in the visible table
if uniqueId in (dataIds - tableIds):
newRow = self._newRowFromUniqueId(_data, obj, uniqueId)
# insert into the table
self.model()._insertRow(uniqueId, newRow)
elif trigger == Notifier.CHANGE:
# uniqueIds in the visible table
if uniqueId in (dataIds & tableIds):
newRow = self._newRowFromUniqueId(_data, obj, uniqueId)
# visible table dataframe update
self.model()._updateRow(uniqueId, newRow)
elif trigger == Notifier.RENAME:
# not sure that I need this yet - should be the same as .CHANGE
pass
except Exception as es:
getLogger().debug2(f'Error updating row in table {es}')
if _update:
getLogger().debug2('<updateRowCallback>', data['notifier'],
self._tableData['tableSelection'],
data['trigger'], data['object'])
_val = self.getSelectedObjects() or []
self._tableSelectionChanged.emit(_val)
return _update
def _selectCurrentCallBack(self, data):
"""Callback from a notifier to highlight the chemical shifts
:param data:
"""
if self._tableBlockingLevel:
return
currentShifts = data['value']
self._selectOnTableCurrentChemicalShifts(currentShifts)
def _selectionChangedCallback(self, selected, deselected):
"""Handle item selection as changed in table - call user callback
Includes checking for clicking below last row
"""
self._changeTableSelection(None)
def _selectOnTableCurrentChemicalShifts(self, currentShifts):
"""Highlight the list of currentShifts on the table
:param currentShifts:
"""
self.highlightObjects(currentShifts)
#=========================================================================================
# Table context menu
#=========================================================================================
def _setContextMenu(self, enableExport=True, enableDelete=True):
"""Subclass guiTable to insert new merge items to top of context menu
"""
super()._setContextMenu(enableExport=enableExport, enableDelete=enableDelete)
# add extra items to the menu
_actions = self.tableMenu.actions()
if _actions:
_topMenuItem = _actions[0]
_topSeparator = self.tableMenu.insertSeparator(_topMenuItem)
self._navigateMenu = self.tableMenu.addMenu('Navigate to:')
self._mergeMenuAction = self.tableMenu.addAction('Merge NmrAtoms', self._mergeNmrAtoms)
self._editMenuAction = self.tableMenu.addAction('Edit NmrAtom', self._editNmrAtom)
# move new actions to the top of the list
self.tableMenu.insertAction(_topSeparator, self._mergeMenuAction)
self.tableMenu.insertAction(self._mergeMenuAction, self._editMenuAction)
def _raiseTableContextMenu(self, pos):
"""Create a new menu and popup at cursor position
Add merge item
"""
selection = self.getSelectedObjects()
data = self.getRightMouseItem()
if (data is not None and not data.empty):
cShift = data.get(DATAFRAME_OBJECT)
currentNmrAtom = cShift.nmrAtom if cShift else None
selection = [ch.nmrAtom for ch in selection or [] if ch.nmrAtom]
_check = (currentNmrAtom and 1 < len(selection) and currentNmrAtom in selection) or False
_option = 'into {}'.format(currentNmrAtom.id if currentNmrAtom else '') if _check else ''
self._mergeMenuAction.setText('Merge NmrAtoms {}'.format(_option))
self._mergeMenuAction.setEnabled(_check)
self._editMenuAction.setText('Edit NmrAtom {}'.format(currentNmrAtom.id if currentNmrAtom else ''))
self._editMenuAction.setEnabled(True if currentNmrAtom else False)
self._addNavigationStripsToContextMenu()
else:
# disabled but visible lets user know that menu items exist
self._mergeMenuAction.setText('Merge NmrAtoms')
self._mergeMenuAction.setEnabled(False)
self._editMenuAction.setText('Edit NmrAtom')
self._editMenuAction.setEnabled(False)
# raise the menu
super()._raiseTableContextMenu(pos)
#=========================================================================================
# Table functions
#=========================================================================================
def _mergeNmrAtoms(self):
"""Merge the nmrAtoms in the selection into the nmrAtom that has been right-clicked
"""
selection = self.getSelectedObjects()
data = self.getRightMouseItem()
if (data is not None and not data.empty) and selection:
cShift = data.get(DATAFRAME_OBJECT)
currentNmrAtom = cShift.nmrAtom if cShift else None
matching = [ch.nmrAtom for ch in selection if ch and ch.nmrAtom and ch.nmrAtom != currentNmrAtom and
ch.nmrAtom.isotopeCode == currentNmrAtom.isotopeCode]
nonMatching = [ch.nmrAtom for ch in selection if ch and ch.nmrAtom and ch.nmrAtom != currentNmrAtom and
ch.nmrAtom.isotopeCode != currentNmrAtom.isotopeCode]
if len(matching) < 1:
showWarning('Merge NmrAtoms', 'No matching isotope codes')
else:
ss = 's' if (len(nonMatching) > 1) else ''
nonMatchingList = '\n\n\n({} nmrAtom{} with non-matching isotopeCode{})'.format(len(nonMatching), ss, ss) if nonMatching else ''
yesNo = showYesNo('Merge NmrAtoms', "Do you want to merge\n\n"
"{} into {}{}".format('\n'.join([ss.id for ss in matching]),
currentNmrAtom.id,
nonMatchingList))
if yesNo:
currentNmrAtom.mergeNmrAtoms(matching)
def _editNmrAtom(self):
"""Show the edit nmrAtom popup for the clicked nmrAtom
"""
data = self.getRightMouseItem()
if data is not None and not data.empty:
cShift = data.get(DATAFRAME_OBJECT)
currentNmrAtom = cShift.nmrAtom if cShift else None
if currentNmrAtom:
from ccpn.ui.gui.popups.NmrAtomPopup import NmrAtomEditPopup
popup = NmrAtomEditPopup(parent=self.mainWindow, mainWindow=self.mainWindow, obj=currentNmrAtom)
popup.exec_()
def _addNavigationStripsToContextMenu(self):
cShift = self._getValidChemicalShift4Callback(self.getSelectedObjects())
self._navigateMenu.clear()
if cShift and cShift.nmrAtom:
name = cShift.nmrAtom.name
if cShift.value is None:
return
value = round(cShift.value, 3)
if self._navigateMenu is not None:
self._navigateMenu.addItem(f'All ({name}:{value})',
callback=partial(self._navigateToChemicalShift,
chemicalShift=cShift,
stripPid=ALL))
self._navigateMenu.addSeparator()
for spectrumDisplay in self.mainWindow.spectrumDisplays:
for strip in spectrumDisplay.strips:
self._navigateMenu.addItem(f'{strip.pid} ({name}:{value})',
callback=partial(self._navigateToChemicalShift,
chemicalShift=cShift,
stripPid=strip.pid))
self._navigateMenu.addSeparator()
def _navigateToChemicalShift(self, chemicalShift, stripPid):
strips = []
if stripPid == ALL:
strips = self.mainWindow.strips
else:
strip = self.application.getByGid(stripPid)
if strip:
strips.append(strip)
if strips:
failedStripPids = []
for strip in strips:
try:
navigateToPositionInStrip(strip,
positions=[chemicalShift.value],
axisCodes=[chemicalShift.nmrAtom.name],
widths=[])
except:
failedStripPids.append(strip.pid)
if len(failedStripPids) > 0:
stripStr = 'strip' if len(failedStripPids) == 1 else 'strips'
strips = ', '.join(failedStripPids)
getLogger().warn(
f'Cannot navigate to position {round(chemicalShift.value, 3)} '
f'in {stripStr}: {strips} '
f'for nmrAtom {chemicalShift.nmrAtom.name}.')
#=========================================================================================
# _CSLTableDelegate - handle editing the table, needs moving
#=========================================================================================
EDIT_ROLE = QtCore.Qt.EditRole
class _CSLTableDelegate(QtWidgets.QStyledItemDelegate):
"""handle the setting of data when editing the table
"""
def __init__(self, parent):
"""Initialise the delegate
:param parent - link to the handling table:
"""
QtWidgets.QStyledItemDelegate.__init__(self, parent)
self.customWidget = False
self._parent = parent
def setEditorData(self, widget, index) -> None:
"""populate the editor widget when the cell is edited
"""
model = index.model()
value = model.data(index, EDIT_ROLE)
if not isinstance(value, (list, tuple)):
value = (value,)
if hasattr(widget, 'setColor'):
widget.setColor(*value)
elif hasattr(widget, 'setData'):
widget.setData(*value)
elif hasattr(widget, 'set'):
widget.set(*value)
elif hasattr(widget, 'setValue'):
widget.setValue(*value)
elif hasattr(widget, 'setText'):
widget.setText(*value)
elif hasattr(widget, 'setFile'):
widget.setFile(*value)
else:
msg = 'Widget %s does not expose "setData", "set" or "setValue" method; ' % widget
msg += 'required for table proxy editing'
raise Exception(msg)
def setModelData(self, widget, mode, index):
"""Set the object to the new value
:param widget - typically a lineedit handling the editing of the cell
:param mode - editing mode:
:param index - QModelIndex of the cell
"""
if hasattr(widget, 'get'):
value = widget.get()
elif hasattr(widget, 'value'):
value = widget.value()
elif hasattr(widget, 'text'):
value = widget.text()
elif hasattr(widget, 'getFile'):
files = widget.selectedFiles()
if not files:
return
value = files[0]
else:
msg = f'Widget {widget} does not expose "get", "value" or "text" method; required for table editing'
raise Exception(msg)
row = index.row()
col = index.column()
try:
# get the sorted element from the dataFrame
df = self._parent._df
_iRow = self._parent.model()._sortOrder[row]
obj = df.iloc[_iRow]['_object']
# set the data which will fire notifiers to populate all tables (including this)
func = self._parent._dataFrameObject.setEditValues[col]
if func and obj:
func(obj, value)
except Exception as es:
getLogger().debug('Error handling cell editing: %i %i - %s %s %s' % (row, col, str(es), self._parent.model()._sortOrder, value))
#=========================================================================================
# main
#=========================================================================================
[docs]def main():
"""Show the chemicalShiftTable module
"""
from ccpn.ui.gui.widgets.Application import newTestApplication
from ccpn.framework.Application import getApplication
# create a new test application
app = newTestApplication(interface='Gui')
application = getApplication()
mainWindow = application.ui.mainWindow
# add a module
_module = ChemicalShiftTableModule(mainWindow=mainWindow)
mainWindow.moduleArea.addModule(_module)
# show the mainWindow
app.start()
if __name__ == '__main__':
"""Call the test function
"""
main()