Source code for ccpn.ui.gui.widgets.PulldownListsForObjects

"""
Generate PulldownListCompoundWidget for project objects; 
set callback's on creation, deletion and rename

"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://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 http://www.ccpn.ac.uk/v3-software/downloads/license")
__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-01-13 17:23:26 +0000 (Thu, January 13, 2022) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: Geerten Vuister $"
__date__ = "$Date: 2017-04-18 15:19:30 +0100 (Tue, April 18, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================

import sys

from ccpn.ui.gui.widgets.CompoundWidgets import PulldownListCompoundWidget
from ccpn.core.lib.Notifiers import Notifier


SELECT = '> Select <'
UNDEFINED = '<Undefined>'

DEBUG = False


class _PulldownABC(PulldownListCompoundWidget):
    """
    Abstract base class for v3 object PulldownListCompound widgets
    """

    # need to subclass these
    _klass = None
    _className = None
    _shortClassName = None
    _attributeName = None
    _currentAttributeName = None

    def __init__(self, parent,
                 mainWindow=None,
                 showBorder=False, orientation='left',
                 minimumWidths=(100, 150), maximumWidths=None, fixedWidths=None,
                 labelText=None,
                 showSelectName=False, selectNoneText=None,
                 callback=None, default=None,
                 sizeAdjustPolicy=None, editable=False,
                 filterFunction=None, useIds=False,
                 setCurrent=False, followCurrent=False,
                 **kwds):
        """
        Create a PulldownListCompoundWidget with callbacks responding to changes in the objects
        in project; not to be used directly, used as a base class for the specific classes for 
        the different V3 objects, as defined below.

        :param parent: parent widget
        :param project: containing project
        :param showBorder: flag to display the border of Frame (True, False)
        :param orientation: flag to determine the orientation of the labelText relative to the pulldown widget.
                            Allowed values: 'left', 'right', 'top', 'bottom'
        :param minimumWidths: tuple of two values specifying the minimum width of the Label and Pulldown widget, respectively
        :param labelText: (optional) text for the Label
        :param texts: (optional) iterable generating text values for the Pulldown
        :param showSelectName: (optional) insert <Select> at the top of the Pulldown
        :param selectNameText: (optional) alternative text for <Select> at the top of the Pulldown
        :param callback: (optional) callback for the Pulldown
        :param default: (optional) initially selected element of the Pulldown (text or index)
        :param editable: If True: allows for editing the value
        :param filterFunction: a function(pids:list)->list for editing the pids shown in the pulldown;
                               returns list of new pids
        :param useIds: If true: use id's in stead of pids
        :param setCurrent: Also set appropriate current attribute when selecting
        :param followCurrent: Follow current attribute; updating when it changes
        :param kwds: (optional) keyword, value pairs for the gridding of Frame
      
        """
        # class needs some attributes to be checked and defined before super()

        if self._attributeName is None:
            raise RuntimeError('%s: _attributeName needs to be defined for proper functioning' % self.__class__.__name__)

        self.mainWindow = mainWindow
        if self.mainWindow:
            self.application = mainWindow.application
            project = self.project = mainWindow.application.project
            self.current = mainWindow.application.current
        else:
            self.application = None
            project = self.project = None
            self.current = None

        self._showSelectName = showSelectName
        self._selectNoneText = selectNoneText
        self._filterFunction = filterFunction
        self._useIds = useIds
        self._userCallback = callback

        if labelText is None:
            labelText = self._className + ':'

        if setCurrent and self._currentAttributeName is None:
            raise ValueError('setCurrent option only valid if _currentAttributeName is defined for class')
        self._setCurrent = setCurrent
        if followCurrent and self._currentAttributeName is None:
            raise ValueError('followCurrent option only valid if _currentAttributeName is defined for class')
        self._followCurrent = followCurrent

        super().__init__(parent=parent, showBorder=showBorder,
                         orientation=orientation,
                         minimumWidths=minimumWidths, maximumWidths=maximumWidths, fixedWidths=fixedWidths,
                         labelText=labelText,
                         texts=self._getPids(),
                         sizeAdjustPolicy=sizeAdjustPolicy,
                         callback=self._callback,
                         editable=editable,
                         **kwds)

        if default is not None:
            if isinstance(default, int):
                pass

            elif isinstance(default, self._klass):
                # objs = self.getObjects()
                # default = objs.index(default) if default in objs else None
                default = self.object2value(default)

            else:
                raise ValueError('default parameter: expected int or "%s" object, got "%s"'
                                 % (self._klass.className, type(default)))

            if default is not None:
                self.select(default, blockSignals=True)

        # add a notifier to update the pulldown list
        if project:
            self._notifier1 = Notifier(project,
                                       [Notifier.CREATE, Notifier.DELETE, Notifier.RENAME],
                                       self._className,
                                       self._updatePulldownList)
            self._notifier1.setDebug(DEBUG)

            self._notifier2 = None
            if self._followCurrent:
                self._notifier2 = Notifier(self.current,
                                           [Notifier.CURRENT],
                                           targetName=self._currentAttributeName,
                                           callback=self._updateFromCurrent
                                           )
                self._notifier2.setDebug(DEBUG)

    @property
    def textList(self):
        """Compatibility with previous implementation
        """
        return self.pulldownList.texts

    def getSelectedObject(self):
        """Return the selected object, or None if not selected or empty
        """
        obj = None
        value = self.getText()
        if value != SELECT and len(value) > 0:
            obj = self.value2object(value)
        return obj

    def getObjects(self):
        """Return a list of objects from the pulldown list
        """
        return [self.value2object(val) for val in self.textList if val != SELECT]

    def getCurrentObject(self):
        """Return relevant attribute from current if _currentAttributeName is defined
        """
        if self._currentAttributeName is None:
            raise RuntimeError('%s: _currentAttributeName needs to be defined for proper functioning' % self.__class__.__name__)
        obj = None
        _tmp = getattr(self.current, self._currentAttributeName)
        if _tmp is not None and len(_tmp) > 0:
            obj = _tmp[0]
        #sys.stderr.write('>>> currentObject:\n', obj)
        return obj

    def getFirstItemText(self):
        result = None
        texts = [txt for txt in self.pulldownList.texts if txt != SELECT]
        if texts:
            result = texts[0]
        return result

    def selectFirstItem(self):
        """Set the table to the first item.
        """
        firstItemName = self.getFirstItemText()
        if firstItemName:
            self.select(firstItemName)

    def object2value(self, obj):
        """Convert object to a value (pid or id), to be displayed
        """
        if obj is None:
            return str(None)
        value = obj.id if self._useIds else obj.pid
        return value

    def value2object(self, value):
        """Convert value to object, using pid or construct a pid from id
        Return None if value does not represent a valid V3 object
        """
        if value is None or len(value) == 0 or value == SELECT:
            return None
        if self._useIds:
            value = self._shortClassName + ':' + value
        obj = self.project.getByPid(value)
        return obj

    def update(self):
        """Public function to update
        """
        if self._followCurrent:
            self._updateFromCurrent()
        else:
            self._updatePulldownList()

    def unRegister(self):
        """Unregister the notifiers; needs to be called when disgarding a instance
        """
        try:
            if self._notifier1 is not None:
                self._notifier1.unRegister()
                del (self._notifier1)
            if self._notifier2 is not None:
                self._notifier2.unRegister()
                del (self._notifier2)
        except:
            pass

    #==============================================================================================
    # Implementation
    #==============================================================================================

    def _getPids(self) -> list:
        """Return a list of pids defined by 'self._attributeName' from project.
        """
        if self.project:
            pids = [self.object2value(obj) for obj in getattr(self.project, self._attributeName)]
            if self._filterFunction:
                pids = self._filterFunction(pids)
            if self._followCurrent:
                # add current if it is not part of the pids
                current = self.getCurrentObject()
                if current is not None:
                    currentPid = self.object2value(current)
                    if currentPid not in pids:
                        pids = [currentPid] + pids
                if current is None and not self._showSelectName:
                    pids = [UNDEFINED] + pids

            if self._showSelectName:
                if self._selectNoneText:
                    select = [self._selectNoneText]
                else:
                    select = [SELECT]
                pids = select + pids
            return pids
        return []

    def _updatePulldownList(self, callbackDict=None):
        """Callback to update the pulldown list; triggered by object creation, deletion or renaming
        """
        if DEBUG: sys.stderr.write('>>> %s._updatePulldownList()\n' % self)
        pids = self._getPids()

        # update the pid list with renamed, created or deleted pids
        lastPulldownItem = self.getText()
        newName = None
        if callbackDict:
            obj = callbackDict[Notifier.OBJECT]
            trigger = callbackDict[Notifier.TRIGGER]
            if trigger == Notifier.DELETE:
                # the object has been notified for delete but still exists so needs to be removed from the list
                if obj.pid in pids:
                    pids.remove(obj.pid)
            elif trigger == Notifier.RENAME:
                if callbackDict[Notifier.OLDPID] == lastPulldownItem:
                    newName = obj.pid

        self.modifyTexts(pids)

        # if the pulldownlist has updated then emit a changed notifier; assumes that the texts are unique
        newPulldownItem = self.getText()
        if lastPulldownItem != newPulldownItem:
            if newName:
                self.select(newName, blockSignals=True)

            else:
                newItem = self.pulldownList.currentIndex()
                self.pulldownList.currentIndexChanged.emit(newItem)

        if DEBUG: sys.stderr.write('  < %s._updatePulldownList()\n' % self)

    def _updateFromCurrent(self, callbackDict=None):
        """Callback to update the selection from current change
        """
        obj = self.getCurrentObject()
        if DEBUG: sys.stderr.write('>>> %s._updateFromCurrent() "%s": %s\n' %
                                   (self, self._currentAttributeName, obj))
        self._updatePulldownList(callbackDict=callbackDict)
        if obj is not None:
            value = self.object2value(obj)
            self.select(value, blockSignals=True)
        else:
            self.setIndex(0, blockSignals=True)
        if DEBUG: sys.stderr.write('  < %s._updateFromCurrent()\n' % self)

    def _callback(self, value):
        """Callback when selecting the pulldown
        """
        if DEBUG: sys.stderr.write('>>> %s._callback() selecting pulldown: %s\n' % (self, value))
        if self._userCallback:
            value = self._userCallback(value)
        if self._setCurrent and value != SELECT and len(value) > 0:
            obj = self.value2object(value)
            if DEBUG: sys.stderr.write('>>> %s._callback() selecting pulldown: setting current.%s to %s\n' %
                                       (self, self._currentAttributeName, obj))
            setattr(self.current, self._currentAttributeName, [obj])
        if DEBUG: sys.stderr.write('  < %s._callback() selecting pulldown\n' % self)

    def __str__(self):
        return '<%s>' % self.__class__.__name__

    @staticmethod
    def onDestroyed(widget):
        if DEBUG: sys.stderr.write('>>> %s being destroyed:\n' % widget)


#==========================================================================================================
# Implementations for the various V3 objects
#==========================================================================================================

def _definedBy(klass):
    """Return relevant attributes from klass
    """
    return (klass, klass.className, klass.shortClassName, klass._pluralLinkName)


[docs]class AtomPulldown(_PulldownABC): """"A PulldownListCompoundWidget class for Atoms """ from ccpn.core.Atom import Atom _klass, _className, _shortClassName, _attributeName = _definedBy(Atom) _currentAttributeName = None
[docs]class CalculationStepPulldown(_PulldownABC): """A PulldownListCompoundWidget class for CalculationSteps """ from ccpn.core.CalculationStep import CalculationStep _klass, _className, _shortClassName, _attributeName = _definedBy(CalculationStep) _currentAttributeName = None
[docs]class ChainPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Chains """ from ccpn.core.Chain import Chain _klass, _className, _shortClassName, _attributeName = _definedBy(Chain) _currentAttributeName = 'chains'
[docs]class ChemicalShiftPulldown(_PulldownABC): """A PulldownListCompoundWidget class for _OldChemicalShifts """ from ccpn.core._OldChemicalShift import _OldChemicalShift _klass, _className, _shortClassName, _attributeName = _definedBy(_OldChemicalShift) _currentAttributeName = '_oldChemicalShifts'
[docs]class ChemicalShiftListPulldown(_PulldownABC): """A PulldownListCompoundWidget class for ChemicalShiftLists """ from ccpn.core.ChemicalShiftList import ChemicalShiftList _klass, _className, _shortClassName, _attributeName = _definedBy(ChemicalShiftList) _currentAttributeName = 'chemicalShiftLists'
[docs]class ComplexPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Complexs """ from ccpn.core.Complex import Complex _klass, _className, _shortClassName, _attributeName = _definedBy(Complex) _currentAttributeName = None
[docs]class DataPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Datas """ from ccpn.core.Data import Data _klass, _className, _shortClassName, _attributeName = _definedBy(Data) _currentAttributeName = None
[docs]class StructureDataPulldown(_PulldownABC): """A PulldownListCompoundWidget class for StructureData """ from ccpn.core.StructureData import StructureData _klass, _className, _shortClassName, _attributeName = _definedBy(StructureData) _currentAttributeName = None
[docs]class IntegralPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Integrals """ from ccpn.core.Integral import Integral _klass, _className, _shortClassName, _attributeName = _definedBy(Integral) _currentAttributeName = 'integrals'
[docs]class IntegralListPulldown(_PulldownABC): """A PulldownListCompoundWidget class for IntegralLists """ from ccpn.core.IntegralList import IntegralList _klass, _className, _shortClassName, _attributeName = _definedBy(IntegralList) _currentAttributeName = None
[docs]class ModelPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Models """ from ccpn.core.Model import Model _klass, _className, _shortClassName, _attributeName = _definedBy(Model) _currentAttributeName = None
[docs]class MultipletPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Multiplets """ from ccpn.core.Multiplet import Multiplet _klass, _className, _shortClassName, _attributeName = _definedBy(Multiplet) _currentAttributeName = 'multiplets'
[docs]class MultipletListPulldown(_PulldownABC): """A PulldownListCompoundWidget class for MultipletLists """ from ccpn.core.MultipletList import MultipletList _klass, _className, _shortClassName, _attributeName = _definedBy(MultipletList) _currentAttributeName = None
[docs]class NmrAtomPulldown(_PulldownABC): """A PulldownListCompoundWidget class for NmrAtoms """ from ccpn.core.NmrAtom import NmrAtom _klass, _className, _shortClassName, _attributeName = _definedBy(NmrAtom) _currentAttributeName = 'nmrAtoms'
[docs]class NmrChainPulldown(_PulldownABC): """A PulldownListCompoundWidget class for NmrChains """ from ccpn.core.NmrChain import NmrChain _klass, _className, _shortClassName, _attributeName = _definedBy(NmrChain) _currentAttributeName = 'nmrChains'
[docs]class NmrResiduePulldown(_PulldownABC): """A PulldownListCompoundWidget class for NmrResidues """ from ccpn.core.NmrResidue import NmrResidue _klass, _className, _shortClassName, _attributeName = _definedBy(NmrResidue) _currentAttributeName = 'nmrResidues'
[docs]class NotePulldown(_PulldownABC): """A PulldownListCompoundWidget class for Notes """ from ccpn.core.Note import Note _klass, _className, _shortClassName, _attributeName = _definedBy(Note) _currentAttributeName = None
[docs]class PeakPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Peaks """ from ccpn.core.Peak import Peak _klass, _className, _shortClassName, _attributeName = _definedBy(Peak) _currentAttributeName = 'peaks'
[docs]class PeakClusterPulldown(_PulldownABC): """A PulldownListCompoundWidget class for PeakClusters """ from ccpn.core.PeakCluster import PeakCluster _klass, _className, _shortClassName, _attributeName = _definedBy(PeakCluster) _currentAttributeName = None
[docs]class PeakListPulldown(_PulldownABC): """A PulldownListCompoundWidget class for PeakLists """ from ccpn.core.PeakList import PeakList _klass, _className, _shortClassName, _attributeName = _definedBy(PeakList) _currentAttributeName = None
[docs]class PseudoDimensionPulldown(_PulldownABC): """A PulldownListCompoundWidget class for PseudoDimensions """ from ccpn.core.PseudoDimension import PseudoDimension _klass, _className, _shortClassName, _attributeName = _definedBy(PseudoDimension) _currentAttributeName = None
[docs]class ResiduePulldown(_PulldownABC): """A PulldownListCompoundWidget class for Residues """ from ccpn.core.Residue import Residue _klass, _className, _shortClassName, _attributeName = _definedBy(Residue) _currentAttributeName = 'residues'
[docs]class RestraintPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Restraints """ from ccpn.core.Restraint import Restraint _klass, _className, _shortClassName, _attributeName = _definedBy(Restraint) _currentAttributeName = None
[docs]class RestraintContributionPulldown(_PulldownABC): """A PulldownListCompoundWidget class for RestraintContributions """ from ccpn.core.RestraintContribution import RestraintContribution _klass, _className, _shortClassName, _attributeName = _definedBy(RestraintContribution) _currentAttributeName = None
[docs]class RestraintTablePulldown(_PulldownABC): """A PulldownListCompoundWidget class for RestraintTables """ from ccpn.core.RestraintTable import RestraintTable _klass, _className, _shortClassName, _attributeName = _definedBy(RestraintTable) _currentAttributeName = None
[docs]class SamplePulldown(_PulldownABC): """A PulldownListCompoundWidget class for Samples """ from ccpn.core.Sample import Sample _klass, _className, _shortClassName, _attributeName = _definedBy(Sample) _currentAttributeName = 'samples'
[docs]class SampleComponentPulldown(_PulldownABC): """A PulldownListCompoundWidget class for SampleComponents """ from ccpn.core.SampleComponent import SampleComponent _klass, _className, _shortClassName, _attributeName = _definedBy(SampleComponent) _currentAttributeName = None
[docs]class SpectrumPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Spectrums """ from ccpn.core.Spectrum import Spectrum _klass, _className, _shortClassName, _attributeName = _definedBy(Spectrum) _currentAttributeName = None
[docs]class SpectrumGroupPulldown(_PulldownABC): """A PulldownListCompoundWidget class for SpectrumGroups """ from ccpn.core.SpectrumGroup import SpectrumGroup _klass, _className, _shortClassName, _attributeName = _definedBy(SpectrumGroup) _currentAttributeName = 'spectrumGroups'
[docs]class SpectrumHitPulldown(_PulldownABC): """A PulldownListCompoundWidget class for SpectrumHits """ from ccpn.core.SpectrumHit import SpectrumHit _klass, _className, _shortClassName, _attributeName = _definedBy(SpectrumHit) _currentAttributeName = 'spectrumHits'
[docs]class SpectrumReferencePulldown(_PulldownABC): """A PulldownListCompoundWidget class for SpectrumReferences """ from ccpn.core.SpectrumReference import SpectrumReference _klass, _className, _shortClassName, _attributeName = _definedBy(SpectrumReference) _currentAttributeName = None
[docs]class StructureEnsemblePulldown(_PulldownABC): """A PulldownListCompoundWidget class for StructureEnsembles """ from ccpn.core.StructureEnsemble import StructureEnsemble _klass, _className, _shortClassName, _attributeName = _definedBy(StructureEnsemble) _currentAttributeName = None
[docs]class DataTablePulldown(_PulldownABC): """A PulldownListCompoundWidget class for DataTables """ from ccpn.core.DataTable import DataTable _klass, _className, _shortClassName, _attributeName = _definedBy(DataTable) _currentAttributeName = None
[docs]class ViolationTablePulldown(_PulldownABC): """A PulldownListCompoundWidget class for ViolationTables """ from ccpn.core.ViolationTable import ViolationTable _klass, _className, _shortClassName, _attributeName = _definedBy(ViolationTable) _currentAttributeName = None
[docs]class CollectionPulldown(_PulldownABC): """A PulldownListCompoundWidget class for Collections """ from ccpn.core.Collection import Collection _klass, _className, _shortClassName, _attributeName = _definedBy(Collection) _currentAttributeName = None
[docs]class SubstancePulldown(_PulldownABC): """A PulldownListCompoundWidget class for Substances """ from ccpn.core.Substance import Substance _klass, _className, _shortClassName, _attributeName = _definedBy(Substance) _currentAttributeName = 'substances'
[docs]class SpectrumDisplayPulldown(_PulldownABC): """A PulldownListCompoundWidget class for SpectrumDisplays """ from ccpn.ui._implementation.SpectrumDisplay import SpectrumDisplay _klass, _className, _shortClassName, _attributeName = _definedBy(SpectrumDisplay) _currentAttributeName = None