"""
Module Documentation here
"""
#=========================================================================================
# 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-28 16:54:26 +0000 (Fri, January 28, 2022) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: Ed Brooksbank $"
__date__ = "$Date: 2021-02-04 11:28:53 +0000 (Thu, February 04, 2021) $"
#=========================================================================================
# Start of code
#=========================================================================================
import os
from functools import partial
from collections import OrderedDict
from PyQt5 import QtWidgets, QtGui
from ccpn.core.lib import Util as ccpnUtil
from ccpn.core.lib.DataStore import DataRedirection, DataStore, PathRedirections
from ccpn.util.Path import aPath, Path
from ccpn.util.Logging import getLogger
from ccpn.framework.Application import getApplication
from ccpn.ui.gui.widgets.Button import Button
from ccpn.ui.gui.widgets.ButtonList import ButtonList
from ccpn.ui.gui.widgets.FileDialog import SpectrumFileDialog
from ccpn.ui.gui.widgets.Label import Label
from ccpn.ui.gui.widgets.LineEdit import LineEdit
from ccpn.ui.gui.widgets.ScrollArea import ScrollArea
from ccpn.ui.gui.widgets.Frame import Frame
from ccpn.ui.gui.guiSettings import getColours, DIVIDER
from ccpn.ui.gui.popups.Dialog import CcpnDialog
from ccpn.ui.gui.widgets.Spacer import Spacer
from ccpn.ui.gui.widgets.HLine import HLine
from ccpn.ui.gui.widgets.RadioButtons import RadioButtons
from ccpn.ui.gui.widgets.Splitter import Splitter
from ccpn.ui.gui.widgets.MessageDialog import showWarning
from ccpn.ui.gui.lib.GuiPath import VALIDROWCOLOUR, ACCEPTROWCOLOUR, REJECTROWCOLOUR, INVALIDROWCOLOUR
from ccpn.core.lib.ContextManagers import undoStackBlocking
from ccpn.util.Logging import getLogger
from ccpn.ui.gui.popups.AttributeEditorPopupABC import getAttributeTipText
LINEEDITSMINIMUMWIDTH = 195
INSIDEDATA = 'insideData'
ALONGSIDEDATA = 'alongsideData'
REMOTEDATA = 'remoteData'
STANDARD = 'standard'
VALIDSTORENAMES = (('$DATA', REMOTEDATA),
('$INSIDEDATA', INSIDEDATA),
('ALONGSIDEDATA', ALONGSIDEDATA))
DIRSEP = '/'
SHOWDATAURLS = True
VALID_ROWCOLOUR = QtGui.QColor('palegreen')
VALID_CHANGED_ROWCOLOUR = QtGui.QColor('darkseagreen')
WARNING_ROWCOLOUR = QtGui.QColor('orange')
INVALID_ROWCOLOUR = QtGui.QColor('lightpink')
INVALID_CHANGED_ROWCOLOUR = QtGui.QColor('red')
[docs]class ValidatorABC(QtGui.QValidator):
def __init__(self, obj, parent=None, callback=None):
QtGui.QValidator.__init__(self, parent=parent)
self.obj = obj
self.callback = callback
self._isValid = True # The result of the validator
[docs] def validate(self, p_str, p_int):
self._isValid = self.isValid(p_str.strip())
if self._isValid:
state = QtGui.QValidator.Acceptable
else:
state = QtGui.QValidator.Intermediate
if self.callback:
self.callback(self)
return state, p_str, p_int
[docs] def isValid(self, value):
"""return True is value is valid; should be subclassed"""
raise NotImplementedError('Implement %s.isValid' % self.__class__.__name__)
[docs]class PathRowABC(object):
"""Implements all functionality for a row with label, text and button to select a file path
"""
validatorClass = None # Requires subclassing
dialogFileMode = 1
LABELWIDGET_MIN_WIDTH = 200
def __init__(self, parentWidget, row, labelText, obj, enabled=True, callback=None):
"""
:param parentWidget:
:param labelText:
:param row:
:param obj: object being displayed
:param callback: func(self) is called when changing value of the dataWidget
"""
if self.validatorClass is None:
raise NotImplementedError('Define %s.validatorClass' % self.__class__.__name__)
self.labelText = labelText
self.obj = obj
self.enabled = enabled
self.callback = None
# if defined called as: callback(self)
self.row = None # Undefined
self.isValid = True
self.validator = None # validator instance of type self.validatorClass
self.initialValue = None
self.labelWidget = None
self.dataWidget = None
self.buttonWidget = None
self.initDone = False
self._addRow(widget=parentWidget, row=row)
self.callback = callback # callback for this row upon change of value /validate()
self.initDone = True
@property
def text(self):
"""Return the text content of the dataWidget"""
return self.getText()
@text.setter
def text(self, value):
self.setText(value)
@property
def hasChanged(self):
"""Return True if the text value has changed"""
return (self.initialValue != self.text) #and not self._firstTime
@property
def isNotValid(self):
return not self.isValid
def _addRow(self, widget, row):
"""Add the row to widget
returns self
"""
self.row = row
self.labelWidget = Label(widget, text=self.labelText, grid=(row, 0))
self.labelWidget.setMinimumWidth(self.LABELWIDGET_MIN_WIDTH)
self.dataWidget = LineEdit(widget, textAlignment='left', grid=(row, 1))
if self.enabled:
self.validator = self.validatorClass(obj=self.obj, parent=self.dataWidget, callback=self._validatorCallback)
self.dataWidget.setValidator(self.validator)
else:
# set to italic/grey
oldFont = self.dataWidget.font()
oldFont.setItalic(True)
self.dataWidget.setFont(oldFont)
self.buttonWidget = Button(widget, grid=(self.row, 2), callback=self._getDialog,
icon='icons/directory')
# initialise
self.initialValue = self.getPath()
self._setDataInWidget(self.initialValue)
self.setEnabled(self.enabled)
return self
def _getDialog(self):
dialogPath = self.getDialogPath()
dialog = SpectrumFileDialog(parent=self.buttonWidget, acceptMode='select', directory=dialogPath)
dialog._show()
choices = dialog.selectedFiles()
if choices is not None and len(choices) > 0 and len(choices[0]) > 0:
newPath = choices[0]
# We sometimes get silly results back; just checking here
if self.validator.isValid(newPath):
self.setPath(newPath)
self._setDataInWidget(newPath)
else:
showWarning("Invalid File", '"%s" is not compatible with %s' % (newPath, self.obj))
def _setDataInWidget(self, path):
"""Populate the dataWidget and validate"""
self.setText(path)
self.validate()
[docs] def setEnabled(self, enable):
"""Enable or disable the row"""
if self.labelWidget is None:
raise RuntimeError('No row widgets defined')
self.enabled = enable
self.labelWidget.setEnabled(self.enabled)
self.dataWidget.setEnabled(self.enabled)
self.buttonWidget.setVisible(self.enabled)
[docs] def setLabel(self, text):
"""Set the labelWidget to text"""
if self.labelWidget is None:
raise RuntimeError('No row widgets defined')
self.labelWidget.setText(text)
[docs] def getText(self):
"""Get the textWidget text"""
if self.dataWidget is None:
raise RuntimeError('No row widgets defined')
return self.dataWidget.text()
[docs] def setText(self, text):
"""Set the textWidget to text"""
if self.dataWidget is None:
raise RuntimeError('No row widgets defined')
self.dataWidget.setText(text)
[docs] def setPath(self, path):
"""Set the path name of the object; requires subclassing"""
pass
[docs] def getPath(self) -> str:
"""Get the path name from the object to edit; requires subclassing"""
pass
[docs] def getDialogPath(self) -> str:
"""Get the directory path to start the selection dialog; optionally can be subclassed"""
dirPath = Path.home()
return str(dirPath)
[docs] def update(self):
"""Call self.path with current value"""
self.setPath(self.getText())
def _validatorCallback(self, validator):
"""Callback for the validator instance; set path if valid
Also call self.callback (if defined)
"""
self.isValid = validator._isValid
if self.isValid and self.initDone: # This avoids setting on initialisation
self.update()
self.colourRow()
if self.callback:
self.callback(self)
[docs] def validate(self):
"""Validate the row, return True if valid"""
if self.validator is not None:
self.validator.validate(self.text, 0)
self.isValid = self.validator._isValid
return self.isValid
[docs] def setColour(self, colour):
"""Set the (base) colour of the dataWidget"""
if isinstance(colour, str):
colour = QtGui.QColor(colour)
if not isinstance(colour, QtGui.QColor):
raise ValueError('Invalid colour ("%s"' % colour)
palette = self.dataWidget.palette()
palette.setColor(QtGui.QPalette.Base, colour)
self.dataWidget.setPalette(palette)
[docs] def colourRow(self):
"""Set colours of row depending on its state
"""
# row is valid
if self.isValid:
if self.hasChanged:
self.setColour(VALID_CHANGED_ROWCOLOUR)
else:
self.setColour(VALID_ROWCOLOUR)
# row is not valid
else:
if self.hasChanged:
self.setColour(INVALID_CHANGED_ROWCOLOUR)
else:
self.setColour(INVALID_ROWCOLOUR)
[docs] def setVisible(self, visible):
"""set visibilty of row"""
self.labelWidget.setVisible(visible)
self.dataWidget.setVisible(visible)
self.buttonWidget.setVisible(visible)
# end class
[docs]class SpectrumValidator(ValidatorABC):
[docs] def isValid(self, value):
"""return True is value is valid;
"""
dataStore, dataSource = self.obj._getDataSourceFromPath(path=value)
return dataStore is not None and dataSource is not None
# end class
[docs]class SpectrumPathRow(PathRowABC):
"""
A class to implement a row for spectrum paths
"""
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
dialogFileMode = 1
validatorClass = SpectrumValidator
def __init__(self, *args, **kwds):
super(SpectrumPathRow, self).__init__(*args, **kwds)
# remember the initial filePath for the popups
self._initialFilePath = str(self.obj.filePath) if self.obj else None
pass
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@property
def initialFilePath(self):
"""Return the initialFilePath if the obj is specified"""
return self._initialFilePath
def _resetFilePath(self):
"""Reset the filePath if populating in popups"""
if self._initialFilePath:
# set the text to the value set when the row was initialised
self._setDataInWidget(self._initialFilePath)
[docs] def getPath(self) -> str:
"""Get the filePath from spectrum"""
return str(self.obj.filePath)
[docs] def setPath(self, path):
"""set the filePath of Spectrum"""
# For speed reasons, we check if it any different from before, or was not valid to start with
oldPath = self.getPath()
if path != oldPath or not self.obj.hasValidPath():
try:
self.obj.filePath = path
return True
except Exception:
getLogger().debug2('ignoring filePath error')
[docs] def getDialogPath(self) -> str:
"""Get the directory path to start the selection;
traverse up the tree to find a valid directory
"""
_path = self.obj.path.parent
atRoot = False
while not _path.exists() and not atRoot:
atRoot = (_path.parent == _path.root)
_path = _path.parent
if atRoot:
_path = aPath('~')
return str(_path)
# end class
# NOT USED
# class DataUrlValidator(ValidatorABC):
#
# def isValid(self, value):
# "return True is value is valid"
# filePath = aPath(value)
# return filePath.exists() and filePath.is_dir()
# # end class
# class UrlPathRow(PathRowABC):
# """
# A class to implement a row for url paths
# """
# validatorClass = DataUrlValidator
# dialogFileMode = 2
#
# def getPath(self):
# return self.obj.url.path
#
# def setPath(self, path):
# # self.obj.url.path = path not allowed?
# oldPath = self.getPath()
# if oldPath != path:
# dataUrl = self.obj
# dataUrl.url = dataUrl.url.clone(path=path)
# # end class
[docs]class RedirectPathRow(PathRowABC):
"""
A class to implement a row for Redirection object
"""
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
dialogFileMode = 2
# validatorClass = DataUrlValidator
[docs] class validatorClass(ValidatorABC):
"""Validator implementation"""
[docs] def isValid(self, value):
"""return True is value is valid"""
filePath = aPath(value)
return filePath.exists() and filePath.is_dir()
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[docs] def getPath(self):
return str(self.obj.path)
[docs] def setPath(self, path):
self.obj.path = aPath(path)
# end class
# Radiobuttons
VALID_SPECTRA = 'valid'
INVALID_SPECTRA = 'invalid'
CHANGED_SPECTRA = 'changed'
ALL_SPECTRA = 'all'
buttons = (ALL_SPECTRA, VALID_SPECTRA, INVALID_SPECTRA, CHANGED_SPECTRA)
# def closeEvent(self, event):
# # don't close if there any empty urls, which cause an api crash
# if not self._badUrls:
# super().closeEvent(event)
# else:
# event.ignore()
# showWarning(str(self.windowTitle()), 'Project contains empty dataUrls')