#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (https://www.ccpn.ac.uk) 2014 - 2022"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
"Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See https://ccpn.ac.uk/software/licensing/")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
"CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
"J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Luca Mureddu $"
__dateModified__ = "$dateModified: 2022-03-07 15:33:28 +0000 (Mon, March 07, 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 QtGui, QtCore, QtWidgets
from PyQt5.QtCore import pyqtSlot
from ccpn.core.Spectrum import Spectrum
from pyqtgraph.dockarea.Dock import Dock
from pyqtgraph.dockarea.DockArea import DockArea, DockDrop
from pyqtgraph.dockarea.Container import Container
from ccpn.util.Logging import getLogger
from ccpn.ui.gui.lib.GuiSpectrumDisplay import GuiSpectrumDisplay
from ccpn.ui.gui.modules.CcpnModule import CcpnModule, MODULENAME, WIDGETSTATE
from ccpn.ui.gui.widgets.DropBase import DropBase
from ccpn.ui.gui.widgets.Label import Label
from ccpn.ui.gui.widgets.SideBar import SideBar, SideBarSearchListView
from ccpn.ui.gui.lib.MenuActions import _openItemObject
from ccpn.ui.gui.widgets.Font import Font, getFontHeight, getFont
from ccpn.ui.gui.widgets.MainWindow import MainWindow
from ccpn.ui.gui.lib.GuiWindow import GuiWindow
from ccpn.ui.gui.guiSettings import getColours, LABEL_FOREGROUND
from ccpn.util.Colour import hexToRgb
from ccpn.ui.gui.lib.mouseEvents import SELECT
from ccpn.ui.gui.widgets.ToolBar import ToolBar
from ccpn.ui.gui.widgets.PlaneToolbar import _StripLabel
from ccpn.ui.gui.widgets.GuiTable import GuiTable
from ccpn.ui.gui.widgets.Icon import Icon
from ccpn.framework.Application import getApplication
from ccpn.util.Common import incrementName
from ccpn.util.Path import aPath
ModuleArea = DockArea
Module = Dock
DropAreaLabel = 'Drop Area'
Failed = 'Failed'
MODULEAREA_IGNORELIST = (ToolBar, _StripLabel, GuiTable)
[docs]class TempAreaWindow(GuiWindow, MainWindow):
def __init__(self, area, mainWindow=None, **kwargs):
MainWindow.__init__(self, **kwargs)
GuiWindow.__init__(self, mainWindow.application)
self.setCentralWidget(area)
self.tempModuleArea = area
self.mainModuleArea = self.tempModuleArea.home
self.mainWindow = mainWindow
self.application = mainWindow.application
self.project = mainWindow.application.project
self.current = mainWindow.application.current
self._setShortcuts()
self.setMouseMode(SELECT)
# install handler to resize when moving between displays
self.window().windowHandle().screenChanged.connect(self._screenChangedEvent)
_height = max(5, (getFontHeight(size='SMALL') or 15) // 3)
_vName = Icon('icons/vertical-split')
_hName = Icon('icons/horizontal-split')
path1 = aPath(_vName._filePath).as_posix()
path2 = aPath(_hName._filePath).as_posix()
self.setStyleSheet("""QSplitter {background-color: transparent; }
QSplitter::handle:vertical {background-color: transparent; height: %dpx; image: url(%s); }
QSplitter::handle:horizontal {background-color: transparent; width: %dpx; image: url(%s); }
""" % (_height, path1, _height, path2))
@pyqtSlot()
def _screenChangedEvent(self, *args):
self._screenChanged(*args)
self.update()
def _screenChanged(self, *args):
getLogger().debug2('tempAreaWindow screenchanged')
project = self.application.project
for spectrumDisplay in project.spectrumDisplays:
if spectrumDisplay.isDeleted:
continue
for strip in spectrumDisplay.strips:
if not strip.isDeleted:
strip.refreshDevicePixelRatio()
# NOTE:ED - set pixelratio for extra axes
if hasattr(spectrumDisplay, '_rightGLAxis'):
spectrumDisplay._rightGLAxis.refreshDevicePixelRatio()
if hasattr(spectrumDisplay, '_bottomGLAxis'):
spectrumDisplay._bottomGLAxis.refreshDevicePixelRatio()
[docs] def closeEvent(self, *args, **kwargs):
from ccpn.ui.gui.modules.PythonConsoleModule import PythonConsoleModule
for module in self.tempModuleArea.ccpnModules:
if isinstance(module, PythonConsoleModule):
# move the PythonConsole back to the main ModuleArea or get a C++ error
mainArea = self.mainWindow.moduleArea
mainArea.addModule(module)
module.hide()
else:
module._closeModule()
if self.tempModuleArea in self.mainModuleArea.tempAreas:
self.mainModuleArea.tempAreas.remove(self.tempModuleArea)
# minimise event required here to notify Qt to exit from fullscreen mode
self.showNormal()
self.close()
[docs]class CcpnModuleArea(ModuleArea, DropBase):
def __init__(self, mainWindow, **kwargs):
super().__init__(mainWindow, **kwargs)
DropBase._init(self, acceptDrops=True)
self.mainWindow = mainWindow # a link back to the parent MainWindow
self.application = getApplication() # this will enable to create testing ModuleArea/Modules without mainWindow/project/application
self.preferences = None
if self.application:
self.preferences = self.application.preferences
self.moveModule = self.moveDock
self.setContentsMargins(0, 0, 0, 0)
self.currentModuleNames = []
self._modulesNames = {}
self._ccpnModules = []
self._modules = {} # don't use self.docks, is not updated when removing docks
self._openedSpectrumDisplays = [] # keep track of the order of opened spectrumDisplays
self._seenModuleStates = {} # {className: {moduleName:'', state:widgetsState}}
# self.setAcceptDrops(True) GWV not needed; handled by DropBase init
self.textLabel = DropAreaLabel
self.fontLabel = getFont(size='MAXIMUM')
# if self.mainWindow:
# self.fontLabel = self.mainWindow.application._fontSettings.helveticaBold36
# else: #can be None. for example for testing when developing new GUI modules. Cannot crash just for a font label!
# self.fontLabel = Font('Helvetica', 36, bold=False)
colours = getColours()
self.colourLabel = hexToRgb(colours[LABEL_FOREGROUND])
self._dropArea = None # Needed to know where to add a newmodule when dropping a pid from sideBar
if self._container is None:
for i in self.children():
if isinstance(i, Container):
self._container = i
# def moveDock(self, module, position, neighbor, initTime=False):
# """
# Move an existing Dock to a new location.
# """
#
# if not initTime:
# previousArea = module.getDockArea()
# if previousArea != self:
# if module.maximised:
# module.toggleMaximised()
#
# super().moveDock(module,position,neighbor)
[docs] def dropEvent(self, event, *args):
data = self.parseEvent(event)
source = event.source()
# drop an item from the sidebar onto the drop area
if DropBase.PIDS in data and isinstance(data['event'].source(), (SideBar, SideBarSearchListView)):
# process Pids
self.mainWindow._processPids(data, position=self.dropArea)
elif DropBase.URLS in data:
objs = self.mainWindow._processDroppedItems(data)
# dropped spectra will automatically open from here
spectra = [obj for obj in objs if isinstance(obj, Spectrum)]
_openItemObject(self.mainWindow, spectra, position=self.dropArea)
if hasattr(source, 'implements') and source.implements('dock'):
DockArea.dropEvent(self, event, *args)
# reset the dock area
self.dropArea = None
self.overlay.setDropArea(self.dropArea)
event.accept()
def _maximisedAttrib(self, widget):
try:
getattr(widget, 'maximised')
return True
except:
return False
[docs] def findMaximisedDock(self, event):
result = None
targetWidgets = [widget for widget in self.findChildren(QtWidgets.QWidget) if self._maximisedAttrib(widget)]
maximisedWidgets = [widget for widget in targetWidgets if widget.maximised == True]
if len(maximisedWidgets) > 0:
result = maximisedWidgets[0]
return result
[docs] def dragEnterEvent(self, *args):
event = args[0]
maximisedModule = self.findMaximisedDock(event)
if maximisedModule is not None:
source = event.source()
sourceParentModule = None
try:
sourceParentModule = source._findModule()
except:
pass
if sourceParentModule is not maximisedModule:
maximisedModule.handleDragToMaximisedModule(event)
return
event = args[0]
data = self.parseEvent(event)
if DropBase.PIDS in data and isinstance(data['event'].source(), (SideBar, SideBarSearchListView)):
DockArea.dragEnterEvent(self, *args)
event.accept()
else:
if isinstance(data['source'], MODULEAREA_IGNORELIST):
event.ignore()
else:
DockDrop.dragEnterEvent(self, *args)
event.accept()
[docs] def dragLeaveEvent(self, *args):
event = args[0]
maximisedWidget = self.findMaximisedDock(event)
if maximisedWidget is not None:
maximisedWidget.finishDragToMaximisedModule(event)
DockArea.dragLeaveEvent(self, *args)
event.accept()
[docs] def dragMoveEvent(self, *args):
event = args[0]
maximisedWidget = self.findMaximisedDock(event)
if maximisedWidget is not None:
maximisedWidget.handleDragToMaximisedModule(event)
return
event = args[0]
DockArea.dragMoveEvent(self, *args)
event.accept()
def _paint(self, ev):
p = QtGui.QPainter(self)
# set font
p.setFont(self.fontLabel)
# set colour
p.setPen(QtGui.QColor(*self.colourLabel))
# set size
rgn = self.contentsRect()
rgn = QtCore.QRect(rgn.left(), rgn.top(), rgn.width(), rgn.height())
align = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter
self.hint = p.drawText(rgn, align, DropAreaLabel)
p.end()
[docs] def paintEvent(self, ev):
"""
Draws central label
"""
if not self.ccpnModules:
self._paint(ev)
elif len(self.ccpnModules) == len(self._tempModules()):
# means all modules are pop-out, so paint the label in the main module area
self._paint(ev)
elif all([m.isHidden() for m in self.ccpnModules]):
# means all modules are hidden
self._paint(ev)
def _isNameAvailable(self, name):
"""
Check if the name is not already taken
"""
return name not in self.modules.keys()
def _incrementModuleName(self, name, splitter):
""" fetch an incremented name if not already taken. """
names = list(self.modules.keys())
while name in names:
name = incrementName(name, splitter)
return name
@property
def ccpnModules(self) -> list:
"""return all current modules in area"""
return self._ccpnModules
@ccpnModules.getter
def ccpnModules(self):
if self is not None:
ccpnModules = list(self.findAll()[1].values())
return ccpnModules
@property
def modules(self) -> dict:
"""return all current modules in area as a dictionary. Don't use self.docks"""
return self._modules
@ccpnModules.getter
def modules(self):
if self is not None:
modules = self.findAll()[1]
return modules
return {}
@property
def spectrumDisplays(self):
"""
Return the list of opened spectrumDisplays in the order of their opening.
Contrary to mainWindow.spectrumDisplays that return in alphabetical order.
"""
return [x for x in self._openedSpectrumDisplays if not x.isDeleted]
[docs] def repopulateModules(self):
"""
Repopulate all modules to globally refresh all pulldowns, etc.
"""
modules = self.ccpnModules
for module in modules:
if hasattr(module, '_repopulateModule'):
module._repopulateModule()
def _tempModules(self):
""":return list of modules in temp Areas """
return [a.ccpnModules for a in self.tempAreas]
[docs] def addModule(self, module, position=None, relativeTo=None, **kwds):
"""With these settings the user can close all the modules from the label 'close module' or pop up and
when re-add a new module it makes sure there is a container available.
"""
if module is None:
raise RuntimeError('No module given')
wasMaximised = False
# seems to add too many containers if relativeTo is None
if not relativeTo:
relativeTo = self
for oldModule in self.modules.values():
if oldModule.maximised:
oldModule.toggleMaximised()
wasMaximised = True
if not module._restored:
if not isinstance(module, GuiSpectrumDisplay): #
if not module._onlySingleInstance:
nextAvailableName = self._incrementModuleName(module.titleName, module._nameSplitter)
module.renameModule(nextAvailableName)
## reset widgets as last time the module was opened
self._restoreAsTheLastSeenModule(module)
else:
self._openedSpectrumDisplays.append(module)
# test that only one instance of the module is opened
if hasattr(type(module), '_alreadyOpened'):
_alreadyOpened = getattr(type(module), '_alreadyOpened')
if _alreadyOpened is True:
if hasattr(type(module), '_onlySingleInstance'):
getLogger().warning('Only one instance of %s allowed' % str(module.name))
return
setattr(type(module), '_alreadyOpened', True)
setattr(type(module), '_currentModule', module) # remember the module
if position is None:
position = 'top'
# store original area that the dock will return to when un-floated (not strictly necessary here)
if not self.temporary:
module.orig_area = self
## Determine the container to insert this module into.
## If there is no neighbor, then the container is the top.
if relativeTo is None or relativeTo is self:
if self.topContainer is None:
container = self
neighbor = None
else:
container = self.topContainer
neighbor = None
else:
if isinstance(relativeTo, str):
relativeTo = self.docks[relativeTo]
container = self.getContainer(relativeTo)
if container is None:
raise TypeError("Dock %s is not contained in a DockArea; cannot add another dock relative to it." % relativeTo)
neighbor = relativeTo
## what container type do we need?
neededContainer = {
'bottom': 'vertical',
'top' : 'vertical',
'left' : 'horizontal',
'right' : 'horizontal',
'above' : 'tab',
'below' : 'tab'
}[position]
if neededContainer != container.type() and container.type() == 'tab':
neighbor = container
container = container.container()
## Decide if the container we have is suitable.
## If not, insert a new container inside.
if neededContainer != container.type():
if neighbor is None:
container = self.addContainer(neededContainer, self.topContainer)
else:
container = self.addContainer(neededContainer, neighbor)
## Insert the new dock before/after its neighbor
insertPos = {
'bottom': 'after',
'top' : 'before',
'left' : 'before',
'right' : 'after',
'above' : 'before',
'below' : 'after'
}[position]
module.area = self
old = module.container()
container.insert(module, insertPos, neighbor)
if old is not None:
old.apoptose()
self.docks[module.moduleName] = module
#module.label.sigDragEntered.connect(self._dragEntered)
if wasMaximised:
module.toggleMaximised()
return module
def _restoreAsTheLastSeenModule(self, module):
"""
internal.
Called when adding a new module to the mainArea, but not on restoring from a disk state.
"""
if self.preferences:
if self.preferences.appearance.rememberLastClosedModuleState:
seenModule = self._seenModuleStates.get(module.className)
if seenModule:
name = seenModule.get(MODULENAME, module.titleName)
state = seenModule.get(WIDGETSTATE, {})
nextAvailableName = self._incrementModuleName(name, module._nameSplitter)
module.renameModule(nextAvailableName)
module.restoreWidgetsState(**state)
def _getModulesOnActiveNameEditing(self):
modules = []
for module in self.ccpnModules:
if hasattr(module.label, 'nameEditor'):
if module.label.nameEditor.isVisible():
modules.append(module)
return modules
def _updateSpectrumDisplays(self):
self._openedSpectrumDisplays = [x for x in self._openedSpectrumDisplays if
x in self.mainWindow.spectrumDisplays]
def _isNameEditing(self):
"""
True if any module is being renamed
"""
return len(self._getModulesOnActiveNameEditing())>0
def _finaliseAllNameEditing(self):
for module in self._getModulesOnActiveNameEditing():
module.label._renameLabel()
[docs] def moveDock(self, dock, position, neighbor):
"""
Move an existing Dock to a new location.
"""
## Moving to the edge of a tabbed dock causes a drop outside the tab box
if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab':
neighbor = neighbor.container()
self.addModule(dock, position, neighbor)
moveModule = moveDock
[docs] def makeContainer(self, typ):
# stop the child containers from collapsing
new = super(CcpnModuleArea, self).makeContainer(typ)
new.setChildrenCollapsible(False)
return new
[docs] def getContainer(self, obj):
if obj is None:
return self
if obj.container() is None:
for i in self.children():
if isinstance(i, Container):
self._container = i
return obj.container()
[docs] def apoptose(self, propagate=True):
# remove top container if possible, close this area if it is temporary.
if self.topContainer is None or self.topContainer.count() == 0:
self.topContainer = None
if self.temporary and self.home:
self.home.removeTempArea(self)
def _closeOthers(self, moduleToClose):
modules = [module for module in self.ccpnModules if module != moduleToClose]
for module in modules:
module._closeModule()
def _closeAll(self):
for module in self.ccpnModules:
module._closeModule()
## docksOnly is used for in memory save and restore for the module maximise save and restore system
## if docksOnly we are saving state to memory and are maximising or restoring docks
[docs] def saveState(self, docksOnly=False):
"""
Return a serialized (storable) representation of the state of
all Docks in this DockArea."""
state = {}
try:
getLogger().info('Saving V3.1 Layout')
state = {'main' : ('area', self.childState(self.topContainer, docksOnly), {'id': id(self)}), 'floats': [],
'version': "3.1", 'inMemory': docksOnly}
for a in self.tempAreas:
if a is not None:
geo = a.win.geometry()
geo = (geo.x(), geo.y(), geo.width(), geo.height())
areaState = ('area', self.childState(a.topContainer, docksOnly), {'geo': geo, 'id': id(a)})
state['floats'].append(areaState)
except Exception as e:
getLogger().warning('Impossible to save layout. %s' % e)
return state
[docs] def childState(self, obj, docksOnly=False):
if isinstance(obj, Dock):
# GST for maximise restore syste
# maximiseState = {'maximised' : obj.maximised, 'maximiseRestoreState' : obj.maximiseRestoreState, 'titleBarHidden' : obj.titleBarHidden}
maximiseState = {}
# if docksOnly:
# objWidgetsState = maximiseState
# else:
objWidgetsState = dict(obj.widgetsState, **maximiseState)
return ('dock', obj.name(), objWidgetsState)
else:
childs = []
if obj is not None:
for i in range(obj.count()):
try:
widg = obj.widget(i)
if not docksOnly or (docksOnly and isinstance(widg, (Dock, Container))):
if not widg.isHidden():
childList = self.childState(widg, docksOnly)
childs.append(childList)
except Exception as es:
getLogger().warning('Error accessing widget: %s - %s - %s' % (str(es), widg, obj))
return (obj.type(), childs, obj.saveState())
[docs] def addTempArea(self):
if self.home is None:
area = CcpnModuleArea(mainWindow=self.mainWindow)
area.temporary = True
area.home = self
self.tempAreas.append(area)
win = TempAreaWindow(area, mainWindow=self.mainWindow)
area.win = win
win.show()
else:
area = self.home.addTempArea()
# print "added temp area", area, area.window()
return area
[docs] def restoreState(self, state, restoreSpectrumDisplay=False):
"""
Restore Dock configuration as generated by saveState.
Note that this function does not create any Docks--it will only
restore the arrangement of an existing set of Docks.
"""
modulesNames = [m.name() for m in self.ccpnModules]
version = "3.0"
floatContainer = 'float'
if 'version' in state:
version = state['version']
floatContainer = 'floats'
getLogger().debug('Reading from V%s layout format.' % version)
if 'main' in state:
## 1) make dict of all docks and list of existing containers
containers, docks = self.findAll()
# GST this appears to be important for the in memory save and restore code
if self.home is None:
oldTemps = self.tempAreas[:]
else:
oldTemps = self.home.tempAreas[:]
if state['main'] is not None:
# 2) create container structure, move docks into new containers
self._buildFromState(modulesNames, state['main'], docks, self, restoreSpectrumDisplay=restoreSpectrumDisplay)
## 3) create floating areas, populate
for s in state[floatContainer]:
a = None
# for maximise and restore code
if version == "3.1":
stateId = s[2]['id']
if state['inMemory']:
for temp in oldTemps:
if id(temp) == stateId:
a = temp
oldTemps.remove(temp)
break
if a is None:
a = self.addTempArea()
# GST this indicates a new format file
if version == "3.1":
a._buildFromState(modulesNames, s, docks, a, restoreSpectrumDisplay=restoreSpectrumDisplay)
a.win.setGeometry(*s[2]['geo'])
else:
a._buildFromState(modulesNames, s[0]['main'], docks, a, restoreSpectrumDisplay=restoreSpectrumDisplay)
a.win.setGeometry(*s[1])
## 4) Add any remaining docks to the bottom
for d in docks.values():
self.moveDock(d, 'below', None, initTime=True)
## 5) kill old containers
# if is not none delete
if state['main'] is not None:
for c in containers:
if c is not None:
c.close()
for a in oldTemps:
if a is not None:
a.apoptose()
for d in self.ccpnModules:
if d:
if d.className == Failed:
d.close()
getLogger().warning('Failed to restore: %s' % d.name())
def _buildFromState(self, openedModulesNames, state, docks, root, depth=0, restoreSpectrumDisplay=False):
typ, contents, state = state
if typ == 'dock':
# try:
if contents in openedModulesNames:
obj = docks[contents]
if not isinstance(obj, GuiSpectrumDisplay) or (isinstance(obj, GuiSpectrumDisplay) and restoreSpectrumDisplay):
obj.restoreWidgetsState(**state)
del docks[contents]
else:
obj = CcpnModule(self.mainWindow, contents)
obj.className = Failed
label = Label(obj, 'Failed to restore %s' % contents)
obj.addWidget(label)
self.addModule(obj)
# GST only present in v 3.1 layouts
elif typ == 'area':
if contents is not None:
self._buildFromState(openedModulesNames, contents, docks, root, depth, restoreSpectrumDisplay=restoreSpectrumDisplay)
obj = None
# except KeyError:
# raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
else:
obj = self.makeContainer(typ)
if obj is not None:
if hasattr(root, 'type'):
root.insert(obj)
if typ != 'dock':
for o in contents:
self._buildFromState(openedModulesNames, o, docks, obj, depth + 1, restoreSpectrumDisplay=restoreSpectrumDisplay)
obj.apoptose(propagate=False)
obj.restoreState(state) ## this has to be done later?
[docs] def restoreModuleState(self, layout, module, discard=False):
"""Search the restore tree for a given module
"""
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')
if 'version' in state:
version = state['version']
getLogger().debug('Reading from V%s layout format.' % version)
if 'main' in state and state['main'] is not None:
return self._searchState(module, state['main'], discard)
def _searchState(self, module, state, discard):
"""Traverse through the tree to find the module
"""
try:
typ, contents, state = state
except:
typ, contents = state
if typ == 'dock':
# check the longPid for modules other than spectrumDisplay
if contents == module.longPid:
# found matching module so restore
module.restoreWidgetsState(**state)
return True
elif typ == 'area':
if contents is not None:
return self._searchState(module, contents, discard)
else:
if contents is not None:
found = None
for ii, o in enumerate(contents):
if self._searchState(module, o, discard):
found = ii
break
if found is not None:
if discard:
# remove from the list
contents.pop(ii)
return True