"""
This Module is used to save and restore the gui state of the program.
There are several Try except due to the fragility of Pyqtgraph layouts (containairs) and nested hierarchy of docks/areas etc..
The state is saved in a Json file. The default file is autogenerated when firing the program. It gets auto
"""
#=========================================================================================
# 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-03-10 21:07:05 +0000 (Thu, March 10, 2022) $"
__version__ = "$Revision: 3.1.0 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: Luca Mureddu $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================
import glob
import json
import sys
from collections import OrderedDict as od
from ccpn.core.lib.ContextManagers import undoStackBlocking
from ccpn.util.AttrDict import AttrDict
from ccpn.util.Logging import getLogger
from ccpn.util.Path import aPath
from ccpn.ui.gui.lib.GuiSpectrumDisplay import GuiSpectrumDisplay
from ccpn.framework.PathsAndUrls import CCPN_STATE_DIRECTORY
StateDirName = CCPN_STATE_DIRECTORY
DefaultLayoutFileName = 'Layout.json'
Warning = "warning"
WarningMessage = "Warning. Any changes in this file will be overwritten when saving a new layout."
General = "general"
ApplicationName = "applicationName" # type: str
ApplicationVersion = "applicationVersion"
LayoutVersionName = "LayoutVersion"
LayoutVersion = 'b.6'
SpectrumDisplays = "SpectrumDisplays"
GuiModules = "guiModules"
FileNames = 'fileNames'
LayoutState = "layoutState"
TitleText = 'LayoutFile'
Title = "Title"
DefaultLayoutFile = {
Title : TitleText,
Warning : WarningMessage,
General : {
ApplicationName : "",
ApplicationVersion: "",
LayoutVersionName : "",
},
SpectrumDisplays: [],
GuiModules : [],
FileNames : [],
LayoutState : {}
}
METADATA = '_metadata'
MODULES = 'modules'
def _createLayoutFile(application):
try:
path = aPath(application.statePath) / DefaultLayoutFileName
if General in DefaultLayoutFile:
if ApplicationName in DefaultLayoutFile[General]:
DefaultLayoutFile[General][ApplicationName] = application.applicationName
if ApplicationVersion in DefaultLayoutFile[General]:
DefaultLayoutFile[General][ApplicationVersion] = application.applicationVersion
if LayoutVersionName in DefaultLayoutFile[General]:
DefaultLayoutFile[General][LayoutVersionName] = LayoutVersion
with open(path, "w") as file:
json.dump(DefaultLayoutFile, file, sort_keys=False, indent=4, separators=(',', ': '))
except Exception as e:
getLogger().debug('Impossible to create a layout File.', e)
[docs]def getLayoutFile(application):
path = aPath(application.statePath) / DefaultLayoutFileName
if not path.exists():
_createLayoutFile(application)
return path.asString()
def _updateGeneral(mainWindow, layout):
application = mainWindow.application
applicationName = application.applicationName
applicationVersion = application.applicationVersion
if General in layout:
general = layout.get(General) #getattr(layout, General)
if ApplicationName in general:
# setattr(general, ApplicationName, applicationName)
general[ApplicationName] = applicationName
if ApplicationVersion in general:
# setattr(general, ApplicationVersion, applicationVersion)
general[ApplicationVersion] = applicationVersion
def _updateFileNames(mainWindow, layout):
"""
:param mainWindow:
:param layout:
:return: #updates the fileNames needed for importing the module. list of file name from the full path
"""
guiModules = mainWindow.moduleArea.ccpnModules
names = set()
for guiModule in guiModules:
if not isinstance(guiModule, GuiSpectrumDisplay): #Don't Save spectrum Displays
pyModule = sys.modules[guiModule.__module__]
if pyModule:
file = pyModule.__file__
if file:
names.add(aPath(file).basename)
if len(names) > 0:
if FileNames in layout:
# setattr(layout, FileNames, list(names))
layout[FileNames] = list(names)
def _updateGuiModules(mainWindow, layout):
"""
:param mainWindow:
:param layout:
:return: #updates classNameModuleNameTupleList on layout with list of tuples [(className, ModuleName), (className, ModuleName)]
list of tuples because a multiple modules of the same class type can exist. E.g. two peakListTable modules!
"""
guiModules = mainWindow.moduleArea.ccpnModules
classNames_ModuleNames = [] #list of tuples [(className, ModuleName), (className, ModuleName)]
for module in guiModules:
# if not isinstance(module, GuiSpectrumDisplay): # Displays are not stored here but in the DataModel
if not module.isHidden():
classNames_ModuleNames.append((module.name(), module.className))
if GuiModules in layout:
# if ClassNameModuleName in layout.guiModules:
# setattr(layout.guiModules, ClassNameModuleName, classNames_ModuleNames )
layout[GuiModules] = classNames_ModuleNames
# setattr(layout, GuiModules, classNames_ModuleNames)
def _updateLayoutState(mainWindow, layout):
if LayoutState in layout:
# setattr(layout, LayoutState, mainWindow.moduleArea.saveState())
layout[LayoutState] = mainWindow.moduleArea.saveState()
def _updateSpectrumDisplays(mainWindow, layout):
sds = _getSpectrumDisplaysState(mainWindow.project.spectrumDisplays)
layout[SpectrumDisplays] = sds
def _updateWarning(mainWindow, layout):
if Warning in layout:
# setattr(layout, Warning, WarningMessage)
layout[Warning] = WarningMessage
def _checkLayoutFormat(mainWindow, layout):
if not isinstance(layout, dict):
# assume that this is a 'future' format and remove metadata
getLogger().warning('Layout is not the correct format, converting to a dict')
newLayout = DefaultLayoutFile.copy()
if General in newLayout:
if ApplicationName in newLayout[General]:
newLayout[General][ApplicationName] = mainWindow.application.applicationName
if ApplicationVersion in newLayout[General]:
newLayout[General][ApplicationVersion] = mainWindow.application.applicationVersion
mainWindow.application.layout = newLayout
return mainWindow.application.layout
[docs]def updateSavedLayout(mainWindow):
"""
Updates the application.layout Dict
:param mainWindow: needed to get application
:return: an up to date layout dictionary with the current state of GuiModules
"""
layout = mainWindow.application.layout
layout = _checkLayoutFormat(mainWindow, layout)
_updateGeneral(mainWindow, layout)
_updateSpectrumDisplays(mainWindow, layout)
_updateFileNames(mainWindow, layout)
_updateGuiModules(mainWindow, layout)
_updateLayoutState(mainWindow, layout)
_updateWarning(mainWindow, layout)
[docs]def saveLayoutToJson(mainWindow, jsonFilePath=None):
"""
:param mainWindow:
:param jsonFilePath: User defined file path where to save the layout. Default is in .ccpn/layout/v3Layout.json
:return: None
"""
try:
updateSavedLayout(mainWindow)
layout = mainWindow.application.layout
if not jsonFilePath:
jsonFilePath = getLayoutFile(mainWindow.application)
with open(jsonFilePath, "w") as file:
json.dump(layout, file, sort_keys=False, indent=4, separators=(',', ': '))
except Exception as e:
getLogger().debug('Error saving Layout to "%s": %s' % (jsonFilePath, e))
def _ccpnModulesImporter(path, neededModules):
"""
:param path: fullPath of the directory where are located the CcpnModules files
:return: list of CcpnModule classes
"""
_ccpnModules = []
import pkgutil as _pkgutil
import inspect as _inspect
from ccpn.ui.gui.modules.CcpnModule import CcpnModule
for loader, name, isPpkg in _pkgutil.walk_packages(path):
# print ('>>>loading', name)
# print(neededModules, name)
if name in neededModules:
try:
findModule = loader.find_module(name)
# for neededModule in neededModules:
module = findModule.load_module(name)
# print ('>>>found')
for i, obj in _inspect.getmembers(module):
if _inspect.isclass(obj):
if issubclass(obj, CcpnModule):
if hasattr(obj, 'className'):
# print ('>>> end')
_ccpnModules.append(obj)
# print ('>>> append')
except Exception as es:
getLogger().debug('Error loading module: %s' % str(es))
return _ccpnModules
def _openCcpnModule(mainWindow, ccpnModules, className, moduleName=None):
for ccpnModule in ccpnModules:
if ccpnModule is not None:
if ccpnModule.className == className:
try:
newCcpnModule = ccpnModule(mainWindow=mainWindow, name=moduleName)
newCcpnModule._restored = True
# newCcpnModule.rename(newCcpnModule.name().split('.')[0])
mainWindow.moduleArea.addModule(newCcpnModule)
except Exception as e:
getLogger().debug("Layout restore failed: %s" % e)
def _getApplicationSpecificModules(mainWindow, applicationName) -> list:
"""init imports.
use try except as some applications may not have been distributed
:return a list of modules
"""
modules = []
from ccpn.framework.Application import ANALYSIS_METABOLOMICS, ANALYSIS_STRUCTURE, ANALYSIS_SCREEN
try:
from ccpn.AnalysisAssign import modules as aA
modules.append(aA)
except Exception as e:
getLogger().debug("Import Error for AnalysisAssign, %s" % e)
if applicationName == ANALYSIS_SCREEN:
try:
from ccpn.AnalysisScreen.gui import modules as aS
modules.append(aS)
except Exception as e:
getLogger().debug("Import Error for AnalysisScreen, %s" % e)
if applicationName == ANALYSIS_METABOLOMICS:
try:
from ccpn.AnalysisMetabolomics.ui.gui import modules as aM
modules.append(aM)
except Exception as e:
getLogger().debug("Import Error for AnalysisMetabolomics, %s" % e)
if applicationName == ANALYSIS_STRUCTURE:
try:
from ccpn.AnalysisStructure import modules as aS
modules.append(aS)
except Exception as e:
getLogger().debug("Import Error for AnalysisStructure, %s" % e)
return modules
def _getAvailableModules(mainWindow, layout, neededModules):
from ccpn.ui.gui import modules as gM
if General in layout:
if ApplicationName in layout.general:
applicationName = layout.general.get(ApplicationName) # getattr(layout.general, ApplicationName)
modules = []
if applicationName != mainWindow.application.applicationName:
getLogger().debug('The layout was saved in a different application. Some of the modules might not be loaded.'
'If this happens, start a new project with %s' % applicationName)
else:
modules = _getApplicationSpecificModules(mainWindow, applicationName)
modules.append(gM)
paths = [item.__path__ for item in modules]
ccpnModules = [ccpnModule for path in paths for ccpnModule in _ccpnModulesImporter(path, neededModules)]
return ccpnModules
def _traverse(o, tree_types=(list, tuple)):
"""used to flat the state in a long list """
if isinstance(o, tree_types):
for value in o:
for subvalue in _traverse(value, tree_types):
yield subvalue
else:
yield o
# GST this should be part of the CcpnModuleArea code
# as this and the ModuleArea serialisation code need to
# be modified in parallel
def _getModuleNamesFromState(layoutState):
""" """
names = []
if not layoutState:
return names
lls = []
floatContainer = 'float'
if 'version' in layoutState:
floatContainer = 'floats'
if 'main' in layoutState:
mains = layoutState['main']
lls += list(_traverse(mains))
if floatContainer in layoutState:
flts = layoutState[floatContainer]
lls += list(_traverse(flts))
for i in list(_traverse(flts)):
if isinstance(i, dict):
if 'main' in i:
lls += list(_traverse(i['main']))
excludingList = ['vertical', 'dock', 'horizontal', 'tab', 'main', 'sizes', 'float', 'area']
names = [i for i in lls if i not in excludingList if isinstance(i, str)]
return names
def _openSpectrumDisplays(mainWindow, spectrumDisplaysState):
"""
"""
project = mainWindow.project
with undoStackBlocking() as _: # Do not add to undo/redo stack
for dd in spectrumDisplaysState:
spectrumDisplayKeys = ["displayAxisCodes", "axisOrder", "title",
"positions", "widths", "units", "stripDirection", "is1D"]
fd = {i: dd.get(i) for i in spectrumDisplayKeys}
spectraPids = dd.get("spectra")
spectra = [project.getByPid(p) for p in spectraPids if project.getByPid(p)]
stripsZoomStates = dd.get("stripsZoomStates")
if len(spectra) > 0:
sd = mainWindow.newSpectrumDisplay(spectra[0], axisCodes=fd.get('displayAxisCodes'),
stripDirection=fd.get('stripDirection'))
for sp in spectra[1:]:
sd.displaySpectrum(sp)
if len(stripsZoomStates) > 0:
if len(sd.strips) > 0:
sd.strips[0].restoreZoomFromState(stripsZoomStates[0])
for stripState in stripsZoomStates[1:]:
newStrip = sd.addStrip()
newStrip.restoreZoomFromState(stripState)
else:
project.newSpectrumDisplay(axisCodes=fd.get('displayAxisCodes'), stripDirection=fd.get('stripDirection'))
[docs]def restoreLayout(mainWindow, layout, restoreSpectrumDisplay=False):
## import all the ccpnModules classes specific for the application.
# mainWindow.moduleArea._closeAll()
layout = _checkLayoutFormat(mainWindow, layout)
if restoreSpectrumDisplay:
if SpectrumDisplays in layout:
_openSpectrumDisplays(mainWindow, layout[SpectrumDisplays])
if FileNames in layout:
neededModules = layout.get(FileNames) # getattr(layout, FileNames)
if len(neededModules) > 0:
if GuiModules in layout:
# if ClassNameModuleName in layout.guiModules:
# classNameGuiModuleNameList = getattr(layout.guiModules, ClassNameModuleName)
classNameGuiModuleNameList = layout.get(GuiModules) # getattr(layout, GuiModules)
# Checks if modules are present in the layout file. If not stops it
if not list(_traverse(classNameGuiModuleNameList)):
return
try:
ccpnModules = _getAvailableModules(mainWindow, layout, neededModules)
for classNameGuiModuleName in classNameGuiModuleNameList:
if len(classNameGuiModuleName) == 2:
guiModuleName, className = classNameGuiModuleName
# move the 'skip' to here, instead of in the saveState
if className in ['SpectrumDisplay']:
continue
neededModules.append(className)
_openCcpnModule(mainWindow, ccpnModules, className, moduleName=guiModuleName)
except Exception as e:
getLogger().debug2("Failed to restore Layout %s" % str(e))
if LayoutState in layout:
# Very important step:
# Checks if the all the modules opened are present in the layout state. If not, will not restore the geometries
state = layout.get(LayoutState) # getattr(layout, LayoutState)
if not state:
return
namesFromState = _getModuleNamesFromState(state)
openedModulesName = [i.name() for i in mainWindow.moduleArea.ccpnModules]
compare = list(set(namesFromState) & set(openedModulesName))
if len(openedModulesName) > 0:
if len(compare) == len(openedModulesName):
try:
mainWindow.moduleArea.restoreState(state, restoreSpectrumDisplay=restoreSpectrumDisplay)
except Exception as e:
getLogger().debug2("Layout error: %s" % e)
else:
getLogger().debug2("Layout error: Some of the modules are missing. Geometries could not be restored")
def _getSpectrumDisplaysState(spectrumDisplays):
"""
:return: list of dict with serialisable attributes needed to restore the SpDisplay status
AbstractWrapperClasses will be converted as pid, EG spectrumDisplay.spectra
"""
ll = []
for spectrumDisplay in spectrumDisplays:
dd = spectrumDisplay.getAsDict()
stripDirection = dd.get("stripArrangement")
axisCodes = dd.get("axisCodes")
spectrumDisplayKeys = ["longPid", "axisOrder", "title", "positions", "widths", "units", "is1D"]
fd = {i: dd.get(i) for i in spectrumDisplayKeys}
fd.update({'stripDirection': stripDirection})
fd.update({'displayAxisCodes': axisCodes})
fd.update({'spectra': [sp.pid for sp in spectrumDisplay._getSpectra()]})
# strips informations
stripsZoomStates = [strip.zoomState for strip in spectrumDisplay.strips]
fd.update({"stripsZoomStates": stripsZoomStates})
ll.append(fd)
return ll
def _getFileNameFromPath(path):
name = aPath(path).basename
return name
def _getPredefinedLayouts(dirPath):
# path has to finish with /
sp = (aPath(dirPath) / '*.json').asString()
layoutsFiles = glob.glob(sp)
return layoutsFiles
def _dictLayoutsNamePath(paths):
dd = od()
for path in paths:
name = _getFileNameFromPath(path)
dd[name] = path
return dd
[docs]def isLayoutFile(filePath):
with open(filePath) as fp:
layout = json.load(fp, object_hook=AttrDict)
if layout.get(LayoutState):
return True
return False