#=========================================================================================
# 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-21 17:49:19 +0000 (Mon, March 21, 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
#=========================================================================================
# if not hasattr(systime, 'clock'):
# # NOTE:ED - quick patch to fix bug in pyqt 5.9
# systime.clock = systime.process_time
import json
import os
import sys
import re
import subprocess
import platform
import faulthandler
try:
# set the soft limits for the maximum number of open files
if platform.system() == 'Windows':
import win32file
# set soft limit for Windows
win32file._setmaxstdio(2048)
else:
import resource
# soft limit imposed by the current configuration, hard limit imposed by the operating system.
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
# For the following line to run, you need to execute the Python script as root?
resource.setrlimit(resource.RLIMIT_NOFILE, (2048, hard))
except Exception:
sys.stderr.write(f'Error setting maximum number of files that can be open')
faulthandler.enable()
from typing import List
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer
from distutils.dir_util import copy_tree
from ccpn.core.IntegralList import IntegralList
from ccpn.core.PeakList import PeakList
from ccpn.core.MultipletList import MultipletList
from ccpn.core.Project import Project
from ccpn.core.lib.Notifiers import NotifierBase
from ccpn.core.lib.Pid import Pid
from ccpn.core.lib.ContextManagers import \
logCommandManager, undoBlockWithSideBar, rebuildSidebar
from ccpn.framework.Application import Arguments
from ccpn.framework import Version
from ccpn.framework.AutoBackup import AutoBackup
from ccpn.framework.credits import printCreditsText
from ccpn.framework.Current import Current
from ccpn.framework.lib.pipeline.PipelineBase import Pipeline
from ccpn.framework.Translation import defaultLanguage
from ccpn.framework.Translation import translator
from ccpn.framework.Preferences import Preferences
from ccpn.framework.PathsAndUrls import \
userCcpnMacroPath, \
CCPN_ARCHIVES_DIRECTORY, \
CCPN_STATE_DIRECTORY, \
CCPN_DATA_DIRECTORY, \
CCPN_SPECTRA_DIRECTORY, \
CCPN_PLUGINS_DIRECTORY, \
CCPN_SCRIPTS_DIRECTORY, \
tipOfTheDayConfig, \
ccpnCodePath
from ccpn.ui.gui.Gui import Gui
from ccpn.ui.gui.GuiBase import GuiBase
from ccpn.ui.gui.modules.CcpnModule import CcpnModule
from ccpn.ui.gui.modules.MacroEditor import MacroEditor
from ccpn.ui.gui.widgets import MessageDialog
from ccpn.ui.gui.widgets.FileDialog import MacrosFileDialog
from ccpn.ui.gui.widgets.TipOfTheDay import TipOfTheDayWindow, MODE_KEY_CONCEPTS, loadTipsSetup
from ccpn.ui.gui.popups.RegisterPopup import RegisterPopup
from ccpn.util import Logging
from ccpn.util.Path import Path, aPath, fetchDir
from ccpn.util.AttrDict import AttrDict
from ccpn.util.Common import uniquify, isWindowsOS, isMacOS, isIterable
from ccpn.util.Logging import getLogger
from ccpn.ui.gui import Layout
from ccpn.util.decorators import logCommand
#-----------------------------------------------------------------------------------------
# how frequently to check if license dialog has closed when waiting to show the tip of the day
WAIT_EVENT_LOOP_EMPTY = 0
WAIT_LICENSE_DIALOG_CLOSE_TIME = 100
_DEBUG = False
interfaceNames = ('NoUi', 'Gui')
MAXITEMLOGGING = 4
# For @Ed: sys.excepthook PyQT related code now in Gui.py
[docs]class Framework(NotifierBase, GuiBase):
"""
The Framework class is the base class for all applications.
"""
#-----------------------------------------------------------------------------------------
# to be sub-classed
applicationName = None
applicationVersion = None
#-----------------------------------------------------------------------------------------
def __init__(self, args=Arguments()):
NotifierBase.__init__(self)
GuiBase.__init__(self)
printCreditsText(sys.stderr, self.applicationName, self.applicationVersion)
#-----------------------------------------------------------------------------------------
# register the programme for later with the getApplication() call
#-----------------------------------------------------------------------------------------
from ccpn.framework.Application import ApplicationContainer
container = ApplicationContainer()
container.register(self)
#-----------------------------------------------------------------------------------------
# Key attributes related to the data structure
#-----------------------------------------------------------------------------------------
# Necessary as attribute is queried during initialisation:
self._mainWindow = None
# This is needed to make project available in NoUi (if nothing else)
self._project = None
self._current = None
self._plugins = [] # Hack for now, how should we store these?
# used in GuiMainWindow by startPlugin()
#-----------------------------------------------------------------------------------------
# Initialisations
#-----------------------------------------------------------------------------------------
self.args = args
# NOTE:ED - what is revision for? there are no uses and causes a new error for sphinx documentation unless a string
# self.revision = Version.revision
self.useFileLogger = not self.args.nologging
if self.args.debug3:
self._debugLevel = Logging.DEBUG3
elif self.args.debug2:
self._debugLevel = Logging.DEBUG2
elif self.args.debug:
self._debugLevel = Logging.DEBUG
else:
self._debugLevel = Logging.INFO
self.preferences = Preferences(application=self)
if not self.args.skipUserPreferences:
sys.stderr.write('==> Getting user preferences\n')
self.preferences._getUserPreferences()
self.layout = None # initialised by self._getUserLayout
# GWV these attributes should move to the GUI class (in 3.2x ??)
# For now, they are set in GuiBase and initialised by calls in Gui.__init_
# self._styleSheet = None
# self._colourScheme = None
# self._fontSettings = None
# self._menuSpec = None
# Blocking level for command echo and logging
self._echoBlocking = 0
self._enableLoggingToConsole = True
self._backupTimerQ = None
self._autoBackupThread = None
self._tip_of_the_day = None
self._initial_show_timer = None
self._key_concepts = None
self._registrationDict = {}
self._setLanguage()
self._experimentClassifications = None # initialised in _startApplication once a project has loaded
self._disableUndoException = getattr(self.args, 'disableUndoException', False)
self._ccpnLogging = getattr(self.args, 'ccpnLogging', False)
# register dataLoaders for the first and only time
from ccpn.framework.lib.DataLoaders.DataLoaderABC import getDataLoaders
self._dataLoaders = getDataLoaders()
# register SpectrumDataSource formats for the first and only time
from ccpn.core.lib.SpectrumDataSources.SpectrumDataSourceABC import getDataFormats
self._spectrumDataSourceFormats = getDataFormats()
# get a user interface; nb. ui.start() is called by the application
self.ui = self._getUI()
#-----------------------------------------------------------------------------------------
# properties of Framework
#-----------------------------------------------------------------------------------------
@property
def project(self) -> Project:
""":return currently active project
"""
return self._project
@property
def current(self) -> Current:
"""Current contains selected peaks, selected restraints, cursor position, etc.
see Current.py for detailed descriptiom
:return the Current object
"""
return self._current
@property
def mainWindow(self):
""":returns: MainWindow instance if application has a Gui or None otherwise
"""
if self.hasGui:
return self.ui.mainWindow
return None
@property
def hasGui(self) -> bool:
""":return True if application has a gui"""
return isinstance(self.ui, Gui)
@property
def _isInDebugMode(self) -> bool:
""":return True if either of the debug flags has been set
CCPNINTERNAL: used throughout to check
"""
if self._debugLevel == Logging.DEBUG1 or \
self._debugLevel == Logging.DEBUG2 or \
self._debugLevel == Logging.DEBUG3:
return True
return False
#-----------------------------------------------------------------------------------------
# Useful (?) directories as Path instances
#-----------------------------------------------------------------------------------------
@property
def statePath(self) -> Path:
"""
:return: the absolute path to the state sub-directory of the current project
as a Path instance
"""
return self.project.statePath
@property
def pipelinePath(self) -> Path:
"""
:return: the absolute path to the state/pipeline sub-directory of
the current project as a Path instance
"""
return self.project.pipelinePath
@property
def dataPath(self) -> Path:
"""
:return: the absolute path to the data sub-directory of the current project
as a Path instance
"""
return self.project.dataPath
@property
def spectraPath(self):
"""
:return: the absolute path to the data sub-directory of the current project
as a Path instance
"""
return self.project.spectraPath
@property
def pluginDataPath(self) -> Path:
"""
:return: the absolute path to the data/plugins sub-directory of the
current project as a Path instance
"""
return self.project.pluginDataPath
@property
def scriptsPath(self) -> Path:
"""
:return: the absolute path to the script sub-directory of the current project
as a Path instance
"""
return self.project.scriptsPath
@property
def archivesPath(self) -> Path:
"""
:return: the absolute path to the archives sub-directory of the current project
as a Path instance
"""
return self.project.archivesPath
@property
def tempMacrosPath(self) -> Path:
"""
:return: the absolute path to the ~/.ccpn/macros directory
as a Path instance
"""
return userCcpnMacroPath
#-----------------------------------------------------------------------------------------
# "get" methods
#-----------------------------------------------------------------------------------------
[docs] def get(self, identifier):
"""General method to obtain object (either gui or data) from identifier (pid, gid,
obj-string)
:param identifier: a Pid, Gid or string object identifier
:return a Version-3 core data or graphics object
"""
if identifier is None:
raise ValueError('Expected str or Pid, got "None"')
if not isinstance(identifier, (str, Pid)):
raise ValueError('Expected str or Pid, got "%s" %s' % (identifier, type(identifier)))
identifier = str(identifier)
if len(identifier) == 0:
raise ValueError('Expected str or Pid, got zero-length identifier')
if len(identifier) >= 2 and identifier[0] == '<' and identifier[-1] == '>':
identifier = identifier[1:-1]
return self.project.getByPid(identifier)
[docs] def getByPid(self, pid):
"""Legacy; obtain data object from identifier (pid or obj-string)
replaced by get(identifier).
:param pid: a Pid or string object identifier
:return a Version-3 core data object
"""
return self.get(pid)
[docs] def getByGid(self, gid):
"""Legacy; obtain graphics object from identifier (gid or obj-string)
replaced by get(identifier).
:param gid: a Gid or string object identifier
:return a Version-3 graphics object
"""
return self.get(gid)
#-----------------------------------------------------------------------------------------
# Initialisations and cleanup
#-----------------------------------------------------------------------------------------
def _getUI(self):
"""Get the user interface
:return a Ui instance
"""
if self.args.interface == 'Gui':
from ccpn.ui.gui.Gui import Gui
ui = Gui(application=self)
else:
from ccpn.ui.Ui import NoUi
ui = NoUi(application=self)
return ui
def _startApplication(self):
"""Start the program execution
"""
# NOTE:ED - there are currently issues when loading projects from the command line, or from test cases
# There is no project.application and project is None
# The Logger instantiated is the default logger, required adding extra methods so that, e.g., echoInfo worked
# logCommand has no self.project.application, and requires getApplication() instead
# There is NoUi instantiated yet, so temporarily added loadProject to Ui class called by loadProject below)
# Load / create project on start
if (projectPath := self.args.projectPath) is not None:
project = self.loadProject(projectPath)
else:
project = self._newProject()
if self.preferences.general.checkUpdatesAtStartup and not getattr(self.args, '_skipUpdates', False):
self.ui._checkForUpdates()
if not self.ui._checkRegistration():
return
# Needed in case project load failed
if not project:
sys.stderr.write('==> No project, aborting ...\n')
return
self._experimentClassifications = project.getExperimentClassifications()
self._updateAutoBackup()
sys.stderr.write('==> Done, %s is starting\n' % self.applicationName)
self.ui.startUi()
self._cleanup()
def _cleanup(self):
"""Cleanup at the end of program execution; i.e. once the command loop
has stopped
"""
self._setAutoBackupTime('kill')
#-----------------------------------------------------------------------------------------
# Backup (TODO: need refactoring)
#-----------------------------------------------------------------------------------------
def _updateAutoBackup(self):
# CCPNINTERNAL: also called from preferences popup
if self.preferences.general.autoBackupEnabled:
self._setAutoBackupTime(self.preferences.general.autoBackupFrequency)
else:
self._setAutoBackupTime(None)
def _setAutoBackupTime(self, time):
if self._backupTimerQ is None:
from queue import Queue
self._backupTimerQ = Queue(maxsize=1)
if self._backupTimerQ.full():
self._backupTimerQ.get()
if isinstance(time, (float, int)):
self._backupTimerQ.put(time * 60)
else:
self._backupTimerQ.put(time)
if self._autoBackupThread is None:
self._autoBackupThread = AutoBackup(q=self._backupTimerQ,
backupFunction=self._backupProject)
self._autoBackupThread.start()
def _backupProject(self):
try:
from ccpnmodel.ccpncore.lib.Io import Api as apiIo
apiIo.backupProject(self.project._wrappedData.parent)
backupPath = self.project.backupPath
backupStatePath = fetchDir(backupPath, Layout.StateDirName)
copy_tree(self.statePath, backupStatePath)
layoutFile = os.path.join(backupStatePath, Layout.DefaultLayoutFileName)
Layout.saveLayoutToJson(self.ui.mainWindow, layoutFile)
self.current._dumpStateToFile(backupStatePath)
#Spectra should not be copied over. Dangerous for disk space
# backupDataPath = fetchDir(backupPath, DataDirName)
except Exception as es:
getLogger().warning('Project backup failed with error %s' % es)
#-----------------------------------------------------------------------------------------
def _initialiseProject(self, newProject: Project):
"""Initialise a project and set up links and objects that involve it
"""
from ccpn.core.lib.SpectrumLib import setContourLevelsFromNoise, getDefaultSpectrumColours, _getDefaultOrdering
# # Linkages; need to be here as downstream code depends on it
self._project = newProject
newProject._application = self
# Logging
logger = getLogger()
Logging.setLevel(logger, self._debugLevel)
logger.debug('Framework._initialiseProject>>>')
# Set up current; we need it when restoring project graphics data below
self._current = Current(project=newProject)
# This wraps the underlying data, including the wrapped graphics data
newProject._initialiseProject()
if newProject._isUpgradedFromV2:
getLogger().debug(f'initialising v2 noise and contour levels')
for spectrum in newProject.spectra:
# calculate the new noise level
setContourLevelsFromNoise(spectrum, setNoiseLevel=True,
setPositiveContours=True, setNegativeContours=True,
useSameMultiplier=True)
# set the initial contour colours
(spectrum.positiveContourColour, spectrum.negativeContourColour) = getDefaultSpectrumColours(spectrum)
spectrum.sliceColour = spectrum.positiveContourColour
# set the initial axis ordering
_getDefaultOrdering(spectrum)
newProject._updateApiDataUrl(self.preferences.general.dataPath)
# the project is now ready to use
# Now that all objects, including the graphics are there, restore current
self.current._restoreStateFromFile(self.statePath)
if self.hasGui:
self.ui.initialize(self._mainWindow)
# Get the mainWindow out of the application top level once it's been transferred to ui
del self._mainWindow
else:
# The NoUi version has no mainWindow
self.ui.initialize(None)
#-----------------------------------------------------------------------------------------
def _savePreferences(self):
"""Save the user preferences to file
CCPNINTERNAL: used in PreferencesPopup and GuiMainWindow._close()
"""
self.preferences._saveUserPreferences()
#-----------------------------------------------------------------------------------------
#-----------------------------------------------------------------------------------------
def _setLanguage(self):
# Language, check for command line override, or use preferences
if self.args.language:
language = self.args.language
elif self.preferences.general.language:
language = self.preferences.general.language
else:
language = defaultLanguage
if not translator.setLanguage(language):
self.preferences.general.language = language
# translator.setDebug(True)
sys.stderr.write('==> Language set to "%s"\n' % translator._language)
#-----------------------------------------------------------------------------------------
def _correctColours(self):
"""Autocorrect all colours that are too close to the background colour
"""
from ccpn.ui.gui.guiSettings import autoCorrectHexColour, getColours, CCPNGLWIDGET_HEXBACKGROUND
if self.preferences.general.autoCorrectColours:
project = self.project
# change spectrum colours
for spectrum in project.spectra:
if len(spectrum.axisCodes) > 1:
if spectrum.positiveContourColour and spectrum.positiveContourColour.startswith('#'):
spectrum.positiveContourColour = autoCorrectHexColour(spectrum.positiveContourColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
if spectrum.negativeContourColour and spectrum.negativeContourColour.startswith('#'):
spectrum.negativeContourColour = autoCorrectHexColour(spectrum.negativeContourColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
else:
if spectrum.sliceColour.startswith('#'):
spectrum.sliceColour = autoCorrectHexColour(spectrum.sliceColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
# change peakList colours
for objList in project.peakLists:
objList.textColour = autoCorrectHexColour(objList.textColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
objList.symbolColour = autoCorrectHexColour(objList.symbolColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
# change integralList colours
for objList in project.integralLists:
objList.textColour = autoCorrectHexColour(objList.textColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
objList.symbolColour = autoCorrectHexColour(objList.symbolColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
# change multipletList colours
for objList in project.multipletLists:
objList.textColour = autoCorrectHexColour(objList.textColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
objList.symbolColour = autoCorrectHexColour(objList.symbolColour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
for mark in project.marks:
mark.colour = autoCorrectHexColour(mark.colour,
getColours()[CCPNGLWIDGET_HEXBACKGROUND])
def _initGraphics(self):
"""Set up graphics system after loading
"""
from ccpn.ui.gui.lib import GuiStrip
project = self.project
mainWindow = self.ui.mainWindow
# 20191113:ED Initial insertion of spectrumDisplays into the moduleArea
try:
insertPoint = mainWindow.moduleArea
for spectrumDisplay in mainWindow.spectrumDisplays:
mainWindow.moduleArea.addModule(spectrumDisplay,
position='right',
relativeTo=insertPoint)
insertPoint = spectrumDisplay
except Exception as e:
getLogger().warning('Impossible to restore SpectrumDisplays')
try:
if self.preferences.general.restoreLayoutOnOpening and \
mainWindow.moduleLayouts:
Layout.restoreLayout(mainWindow, mainWindow.moduleLayouts, restoreSpectrumDisplay=False)
except Exception as e:
getLogger().warning('Impossible to restore Layout %s' % e)
# New LayoutManager implementation; awaiting completion
# try:
# from ccpn.framework.LayoutManager import LayoutManager
# layout = LayoutManager(mainWindow)
# path = self.statePath / 'Layout.json'
# layout.restoreState(path)
# layout.saveState()
#
# except Exception as es:
# getLogger().warning('Error restoring layout: %s' % es)
# check that the top moduleArea is correctly formed - strange special case when all modules have
# been moved to tempAreas
mArea = self.ui.mainWindow.moduleArea
if mArea.topContainer is not None and mArea.topContainer._container is None:
getLogger().debug('Correcting empty topContainer')
mArea.topContainer = None
try:
# Initialise colours
# # for spectrumDisplay in project.windows[0].spectrumDisplays: # there is exactly one window
#
# for spectrumDisplay in mainWindow.spectrumDisplays: # there is exactly one window
# pass # GWV: poor solution; removed the routine spectrumDisplay._resetRemoveStripAction()
# initialise any colour changes before generating gui strips
self._correctColours()
except Exception as es:
getLogger().warning(f'Impossible to restore colours - {es}')
# Initialise Strips
for spectrumDisplay in mainWindow.spectrumDisplays:
try:
for si, strip in enumerate(spectrumDisplay.strips):
# temporary to catch bad strips from ordering bug
if not strip:
continue
# get the new tilePosition of the strip - tilePosition is always (x, y) relative to screen stripArrangement
# changing screen arrangement does NOT require flipping tilePositions
# i.e. Y = (across, down); X = (down, across)
# - check delete/undo/redo strips
tilePosition = strip.tilePosition
# move to the correct place in the widget - check stripDirection to display as row or column
if spectrumDisplay.stripArrangement == 'Y':
if True: # tilePosition is None:
spectrumDisplay.stripFrame.layout().addWidget(strip, 0, si) #stripIndex)
strip.tilePosition = (0, si)
# else:
# spectrumDisplay.stripFrame.layout().addWidget(strip, tilePosition[0], tilePosition[1])
elif spectrumDisplay.stripArrangement == 'X':
if True: #tilePosition is None:
spectrumDisplay.stripFrame.layout().addWidget(strip, si, 0) #stripIndex)
strip.tilePosition = (0, si)
# else:
# spectrumDisplay.stripFrame.layout().addWidget(strip, tilePosition[1], tilePosition[0])
elif spectrumDisplay.stripArrangement == 'T':
# NOTE:ED - Tiled plots not fully implemented yet
getLogger().warning('Tiled plots not implemented for spectrumDisplay: %s' % str(spectrumDisplay))
else:
getLogger().warning('Strip direction is not defined for spectrumDisplay: %s' % str(spectrumDisplay))
if not spectrumDisplay.is1D:
for strip in spectrumDisplay.strips:
strip._updatePlaneAxes()
if spectrumDisplay.isGrouped:
# setup the spectrumGroup toolbar
spectrumDisplay.spectrumToolBar.hide()
spectrumDisplay.spectrumGroupToolBar.show()
_spectrumGroups = [project.getByPid(pid) for pid in spectrumDisplay._getSpectrumGroups()]
for group in _spectrumGroups:
spectrumDisplay.spectrumGroupToolBar._forceAddAction(group)
else:
# setup the spectrum toolbar
spectrumDisplay.spectrumToolBar.show()
spectrumDisplay.spectrumGroupToolBar.hide()
spectrumDisplay.setToolbarButtons()
# some of the strips may not be instantiated at this point
# resize the stripFrame to the spectrumDisplay - ready for first resize event
# spectrumDisplay.stripFrame.resize(spectrumDisplay.width() - 2, spectrumDisplay.stripFrame.height())
spectrumDisplay.showAxes(stretchValue=True, widths=True,
minimumWidth=GuiStrip.STRIP_MINIMUMWIDTH)
except Exception as e:
getLogger().warning('Impossible to restore spectrumDisplay(s) %s' % e)
try:
if self.current.strip is None and len(mainWindow.strips) > 0:
self.current.strip = mainWindow.strips[0]
except Exception as e:
getLogger().warning('Error restoring current.strip: %s' % e)
# GST slightly complicated as we have to wait for any license or other
# startup dialogs to close before we display tip of the day
loadTipsSetup(tipOfTheDayConfig, [ccpnCodePath])
self._tip_of_the_day_wait_dialogs = (RegisterPopup,)
self._startupShowTipofTheDay()
#-----------------------------------------------------------------------------------------
def _startupShowTipofTheDay(self):
if self._shouldDisplayTipOfTheDay():
self._initial_show_timer = QTimer(parent=self._mainWindow)
self._initial_show_timer.timeout.connect(self._startupDisplayTipOfTheDayCallback)
self._initial_show_timer.setInterval(0)
self._initial_show_timer.start()
def _canTipOfTheDayShow(self):
result = True
for widget in QApplication.topLevelWidgets():
if isinstance(widget, self._tip_of_the_day_wait_dialogs) and widget.isVisible():
result = False
break
return result
def _startupDisplayTipOfTheDayCallback(self):
is_first_time_tip_of_the_day = self.preferences['general'].setdefault('firstTimeShowKeyConcepts', True)
# GST this waits till any inhibiting dialogs aren't show and then awaits till the event loop is empty
# effectively it swaps between waiting for WAIT_LICENSE_DIALOG_CLOSE_TIME or until the event loop is empty
if not self._canTipOfTheDayShow() or self._initial_show_timer.interval() == WAIT_LICENSE_DIALOG_CLOSE_TIME:
if self._initial_show_timer.interval() == WAIT_EVENT_LOOP_EMPTY:
self._initial_show_timer.setInterval(WAIT_LICENSE_DIALOG_CLOSE_TIME)
else:
self._initial_show_timer.setInterval(WAIT_EVENT_LOOP_EMPTY)
self._initial_show_timer.start()
else:
# this should only happen when the event loop is empty...
if is_first_time_tip_of_the_day:
self._displayKeyConcepts()
self.preferences['general']['firstTimeShowKeyConcepts'] = False
else:
try:
self._displayTipOfTheDay()
except Exception as e:
self._initial_show_timer.stop()
self._initial_show_timer.deleteLater()
self._initial_show_timer = None
raise e
if self._initial_show_timer:
self._initial_show_timer.stop()
self._initial_show_timer.deleteLater()
self._initial_show_timer = None
def _displayKeyConcepts(self):
if not self._key_concepts:
self._key_concepts = TipOfTheDayWindow(mode=MODE_KEY_CONCEPTS)
self._key_concepts.show()
self._key_concepts.raise_()
def _displayTipOfTheDay(self, standalone=False):
# tip of the day allocated standalone already
if self._tip_of_the_day and standalone and self._tip_of_the_day.isStandalone():
self._tip_of_the_day.show()
self._tip_of_the_day.raise_()
# tip of the day hanging around from startup
elif self._tip_of_the_day and standalone and not self._tip_of_the_day.isStandalone():
self._tip_of_the_day.hide()
self._tip_of_the_day.deleteLater()
self._tip_of_the_day = None
if not self._tip_of_the_day:
dont_show_tips = not self.preferences['general']['showTipOfTheDay']
seen_tip_list = []
if not standalone:
seen_tip_list = self.preferences['general']['seenTipsOfTheDay']
self._tip_of_the_day = TipOfTheDayWindow(dont_show_tips=dont_show_tips,
seen_perma_ids=seen_tip_list, standalone=standalone)
self._tip_of_the_day.dont_show.connect(self._tip_of_the_day_dont_show_callback)
if not standalone:
self._tip_of_the_day.seen_tips.connect(self._tip_of_the_day_seen_tips_callback)
self._tip_of_the_day.show()
self._tip_of_the_day.raise_()
def _tip_of_the_day_dont_show_callback(self, dont_show):
self.preferences['general']['showTipOfTheDay'] = not dont_show
def _tip_of_the_day_seen_tips_callback(self, seen_tips):
seen_tip_list = self.preferences['general']['seenTipsOfTheDay']
previous_seen_tips = set(seen_tip_list)
previous_seen_tips.update(seen_tips)
seen_tip_list.clear()
seen_tip_list.extend(previous_seen_tips)
def _shouldDisplayTipOfTheDay(self):
return self.preferences['general'].setdefault('showTipOfTheDay', True)
#-----------------------------------------------------------------------------------------
# Project related methods
#-----------------------------------------------------------------------------------------
def _newProject(self, name: str = 'default') -> Project:
"""Create new, empty project with name
:return a Project instance
"""
# local import to avoid cycles
from ccpn.core.Project import _newProject
newName = re.sub('[^0-9a-zA-Z]+', '', name)
# NB _closeProject includes a gui cleanup call
self._closeProject()
newProject = _newProject(self, name=newName)
self._initialiseProject(newProject) # This also set the linkages
# defer the logging output until the project is fully initialised
if newName != name:
getLogger().info('Removed whitespace from name: %s' % name)
return newProject
# @logCommand('application.') # decorated in ui class
[docs] def newProject(self, name: str = 'default') -> Project:
"""Create new, empty project with name
:return a Project instance
"""
return self.ui.newProject(name)
# @logCommand('application.') # eventually decorated by _loadData()
[docs] def loadProject(self, path=None) -> Project:
"""Load project defined by path
:return a Project instance
"""
return self.ui.loadProject(path)
def _saveProject(self, newPath=None, createFallback=True, overwriteExisting=False) -> bool:
"""Save project to newPath and return True if successful
"""
if self.preferences.general.keepSpectraInsideProject:
self._cloneSpectraToProjectDir()
successful = self.project.save(newPath=newPath, createFallback=createFallback,
overwriteExisting=overwriteExisting)
if not successful:
failMessage = '==> Project save failed'
getLogger().warning(failMessage)
self.ui.mainWindow.statusBar().showMessage(failMessage)
return False
self._getUndo().markSave()
try:
Layout.saveLayoutToJson(self.ui.mainWindow)
except Exception as e:
getLogger().warning('Unable to save Layout %s' % e)
self.current._dumpStateToFile(self.statePath)
return True
# @logCommand('application.') # decorated in ui
[docs] def saveProjectAs(self, newPath, overwrite: bool = False) -> bool:
"""Save project to newPath
:param newPath: new path to save project (str | Path instance)
:param overwrite: flag to indicate overwriting of existing path
:return True if successful
"""
return self.ui.saveProjectAs(newPath=newPath, overwrite=overwrite)
# @logCommand('application.') # decorated in ui
[docs] def saveProject(self) -> bool:
"""Save project.
:return True if successful
"""
return self.ui.saveProject()
def _closeProject(self):
"""Close project and clean up - when opening another or quitting application
"""
# NB: this function must clean up both wrapper and ui/gui
self.deleteAllNotifiers()
if self.ui.mainWindow:
# ui/gui cleanup
self.ui.mainWindow.deleteAllNotifiers()
self.ui.mainWindow._closeMainWindowModules()
self.ui.mainWindow._closeExtraWindowModules()
self.ui.mainWindow.sideBar.clearSideBar()
self.ui.mainWindow.sideBar.deleteLater()
self.ui.mainWindow.deleteLater()
self.ui.mainWindow = None
if self.current:
self.current._unregisterNotifiers()
self._current = None
if self.project is not None:
# Cleans up wrapper project, including graphics data objects (Window, Strip, etc.)
_project = self.project
_project._close()
self._project = None
del (_project)
#-----------------------------------------------------------------------------------------
# Data loaders
#-----------------------------------------------------------------------------------------
def _loadData(self, dataLoaders, maxItemLogging=MAXITEMLOGGING) -> list:
"""Helper function;
calls ui._loadData or ui._loadProject for each dataLoader to load data;
optionally suspend command logging
:param dataLoaders: a list/tuple of dataLoader instances
:param maxItemLogging: flag to set maximum items to log (0 denotes logging all)
:return a list of loaded objects
"""
objs = []
_echoBlocking = maxItemLogging > 0 and len(dataLoaders) > maxItemLogging
if _echoBlocking:
getLogger().info('Loading %d objects, while suppressing command-logging' %
len(dataLoaders))
self._increaseNotificationBlocking()
# Check if there is a dataLoader that creates a new project: in that case, we only want one
_createNew = [dl for dl in dataLoaders if dl.createNewProject]
if len(_createNew) > 1:
raise RuntimeError('Multiple dataLoaders create a new project; can\'t do that')
elif len(_createNew) == 1:
dataLoader = _createNew[0]
with logCommandManager('application.', 'loadProject', dataLoader.path):
# NOTE:ED - move inside ui._loadProject?
if dataLoader.makeArchive:
# make an archive in the project specific archive folder before loading
from ccpn.core.lib.ProjectArchiver import ProjectArchiver
archiver = ProjectArchiver(projectPath=dataLoader.path)
archivePath = archiver.makeArchive()
getLogger().info('==> Project archived to %s' % archivePath)
if not archivePath:
MessageDialog.showWarning('Archive Project',
f'There was a problem creating an archive for {dataLoader.path}',
parent=self.ui.mainWindow
)
result = self.ui._loadProject(dataLoader=dataLoader)
getLogger().info("==> Loaded project %s" % result)
if not isIterable(result):
result = [result]
objs.extend(result)
dataLoaders.remove(dataLoader)
# Now do the remaining ones; put in one undo block
with undoBlockWithSideBar():
for dataLoader in dataLoaders:
with logCommandManager('application.', 'loadData', dataLoader.path):
result = self.ui._loadData(dataLoader=dataLoader)
if not isIterable(result):
result = [result]
objs.extend(result)
if _echoBlocking:
self._decreaseNotificationBlocking()
getLogger().debug('Loaded objects: %s' % objs)
return objs
# @logCommand('application.') # eventually decorated by _loadData()
[docs] def loadData(self, *paths, pathFilter=None) -> list:
"""Loads data from paths.
Optionally filter for dataFormat(s)
:param *paths: argument list of path's (str or Path instances)
:param pathFilter: keyword argument: list/tuple of dataFormat strings
:returns list of loaded objects
"""
return self.ui.loadData(*paths)
# @logCommand('application.') # decorated by ui
[docs] def loadSpectra(self, *paths) -> list:
"""Load all the spectra found in paths.
:param paths: list of paths
:return a list of Spectra instances
"""
return self.ui.loadSpectra(*paths)
def _loadV2Project(self, path) -> List[Project]:
"""Actual V2 project loader
CCPNINTERNAL: called from CcpNmrV2ProjectDataLoader
"""
from ccpn.core.Project import _loadProject
# always close first
self._closeProject()
project = _loadProject(application=self, path=str(path))
self._initialiseProject(project) # This also sets the linkages
# Save the result
try:
project.save()
getLogger().info('==> Saved %s as "%s"' % (project, project.path))
except Exception as es:
getLogger().warning('Failed saving %s (%s)' % (project, str(es)))
return [project]
def _loadV3Project(self, path) -> List[Project]:
"""Actual V3 project loader
CCPNINTERNAL: called from CcpNmrV3ProjectDataLoader
"""
from ccpn.core.Project import _loadProject
# always close first
self._closeProject()
project = _loadProject(application=self, path=path)
self._initialiseProject(project) # This also set the linkages
return [project]
def _loadSparkyFile(self, path: str, createNewProject=True) -> Project:
"""Load Project from Sparky file at path, and do necessary setup
:return Project-instance (either existing or newly created)
CCPNINTERNAL: called from SparkyDataLoader
"""
from ccpn.core.lib.CcpnSparkyIo import SPARKY_NAME, CcpnSparkyReader
sparkyReader = CcpnSparkyReader(self)
dataBlock = sparkyReader.parseSparkyFile(str(path))
sparkyName = dataBlock.getDataValues(SPARKY_NAME, firstOnly=True)
if createNewProject and (dataBlock.getDataValues('sparky', firstOnly=True) == 'project file'):
self._closeProject()
project = self._newProject(sparkyName)
else:
project = self.project
sparkyReader.importSparkyProject(project, dataBlock)
return project
def _loadStarFile(self, dataLoader) -> Project:
"""Load a Starfile, and do necessary setup
:return Project-instance (either existing or newly created)
CCPNINTERNAL: called from StarDataLoader
"""
dataBlock = dataLoader.dataBlock # this will (if required) also read and parse the file
if dataLoader.createNewProject:
self._closeProject()
project = self._newProject(dataBlock.getName())
else:
project = self.project
with rebuildSidebar(application=self):
dataLoader._importIntoProject(project)
return project
def _loadPythonFile(self, path):
"""Load python file path into the macro editor
CCPNINTERNAL: called from PythonDataLoader
"""
mainWindow = self.mainWindow
macroEditor = MacroEditor(mainWindow=mainWindow, filePath=str(path))
mainWindow.moduleArea.addModule(macroEditor, position='top', relativeTo=mainWindow.moduleArea)
return []
def _loadHtmlFile(self, path):
"""Load html file path into a HtmlModule
CCPNINTERNAL: called from HtmlDataLoader
"""
mainWindow = self.mainWindow
path = aPath(path)
mainWindow.newHtmlModule(urlPath=str(path), position='top', relativeTo=mainWindow.moduleArea)
return []
def _cloneSpectraToProjectDir(self):
""" Keep a copy of spectra inside the project directory "myproject.ccpn/data/spectra".
This is useful when saving the project in an external driver and want to keep the spectra together with the project.
"""
from shutil import copyfile
try:
for spectrum in self.project.spectra:
oldPath = spectrum.filePath
# For Bruker need to keep all the tree structure.
# Uses the fact that there is a folder called "pdata" and start to copy from the dir before.
ss = oldPath.split('/')
if 'pdata' in ss:
brukerDir = os.path.join(os.sep, *ss[:ss.index('pdata')])
brukerName = brukerDir.split('/')[-1]
os.mkdir(os.path.join(self.spectraPath, brukerName))
destinationPath = os.path.join(self.spectraPath, brukerName)
copy_tree(brukerDir, destinationPath)
clonedPath = os.path.join(destinationPath, *ss[ss.index('pdata'):])
# needs to repoint the path but doesn't seem to work!! troubles with $INSIDE!!
# spectrum.filePath = clonedPath
else:
# copy the file and or other files containing params
from ntpath import basename
pathWithoutFileName = os.path.join(os.sep, *ss[:ss.index(basename(oldPath))])
fullpath = os.path.join(pathWithoutFileName, basename(oldPath))
import glob
otherFilesWithSameName = glob.glob(fullpath + ".*")
clonedPath = os.path.join(self.spectraPath, basename(oldPath))
for otherFileTocopy in otherFilesWithSameName:
otherFilePath = os.path.join(self.spectraPath, basename(otherFileTocopy))
copyfile(otherFileTocopy, otherFilePath)
if oldPath != clonedPath:
copyfile(oldPath, clonedPath)
# needs to repoint the path but doesn't seem to work!! troubles with $INSIDE!!
# spectrum.filePath = clonedPath
except Exception as e:
getLogger().debug(str(e))
#-----------------------------------------------------------------------------------------
# NEF-related code
#-----------------------------------------------------------------------------------------
def _loadNefFile(self, dataLoader) -> Project:
"""Load NEF file defined by dataLoader instance
:param dataLoader: a NefDataLoader instance
:return Project instance (either newly created or the existing)
CCPNINTERNAL: called from NefDataLoader.load()
"""
if dataLoader.createNewProject:
project = self._newProject(dataLoader.nefImporter.getName())
else:
project = self.project
# TODO: find a different solution for this
with rebuildSidebar(application=self):
dataLoader._importIntoProject(project=project)
return project
def _exportNEF(self):
"""
Export the current project as a Nef file
Temporary routine because I don't know how else to do it yet
"""
from ccpn.ui.gui.popups.ExportNefPopup import ExportNefPopup
from ccpn.framework.lib.ccpnNef.CcpnNefIo import NEFEXTENSION
_path = aPath(self.preferences.general.userWorkingPath or '~').filepath / (self.project.name + NEFEXTENSION)
dialog = ExportNefPopup(self.ui.mainWindow,
mainWindow=self.ui.mainWindow,
selectFile=_path,
fileFilter='*.nef',
minimumSize=(400, 550))
# an exclusion dict comes out of the dialog as it
result = dialog.exec_()
if not result:
return
nefPath = result['filename']
flags = result['flags']
pidList = result['pidList']
# flags are skipPrefixes, expandSelection
skipPrefixes = flags['skipPrefixes']
expandSelection = flags['expandSelection']
includeOrphans = flags['includeOrphans']
self.project.exportNef(nefPath,
overwriteExisting=True,
skipPrefixes=skipPrefixes,
expandSelection=expandSelection,
includeOrphans=includeOrphans,
pidList=pidList)
def _getRecentProjectFiles(self, oldPath=None) -> list:
"""Get and return a list of recent project files, setting reference to
self as first element, unless it is a temp project
update the preferences with the new list
CCPNINTERNAL: called by MainWindow
"""
project = self.project
path = project.path
recentFiles = self.preferences.recentFiles
if not project.isTemporary:
if path in recentFiles:
recentFiles.remove(path)
elif oldPath in recentFiles:
recentFiles.remove(oldPath)
elif len(recentFiles) >= 10:
recentFiles.pop()
recentFiles.insert(0, path)
recentFiles = uniquify(recentFiles)
self.preferences.recentFiles = recentFiles
return recentFiles
#-----------------------------------------------------------------------------------------
# undo/redo
#-----------------------------------------------------------------------------------------
[docs] @logCommand('application.')
def undo(self):
if self.project._undo.canUndo():
with MessageDialog.progressManager(self.ui.mainWindow, 'performing undo'):
self.project._undo.undo()
else:
getLogger().warning('nothing to undo')
[docs] @logCommand('application.')
def redo(self):
if self.project._undo.canRedo():
with MessageDialog.progressManager(self.ui.mainWindow, 'performing redo'):
self.project._undo.redo()
else:
getLogger().warning('nothing to redo.')
def _getUndo(self):
"""Return the undo object for the project
"""
if self.project:
return self.project._undo
else:
raise RuntimeError('Error: undefined project')
def _increaseNotificationBlocking(self):
self._echoBlocking += 1
def _decreaseNotificationBlocking(self):
if self._echoBlocking > 0:
self._echoBlocking -= 1
else:
raise RuntimeError('Error: decreaseNotificationBlocking, already at 0')
#-----------------------------------------------------------------------------------------
# Archive code
#-----------------------------------------------------------------------------------------
[docs] @logCommand('application.')
def saveToArchive(self) -> Path:
"""Archive the project.
:return location of the archive as a Path instance
"""
archivePath = self.project.saveToArchive()
return archivePath
[docs] @logCommand('application')
def restoreFromArchive(self, archivePath) -> Project:
"""Restore a project from archive path
:return the restored project or None on error
"""
from ccpn.core.lib.ProjectArchiver import ProjectArchiver
archiver = ProjectArchiver(projectPath=self.project.path)
if (_newProjectPath := archiver.restoreArchive(archivePath=archivePath)) is not None and \
(_newProject := self.loadProject(_newProjectPath)) is not None:
getLogger().info('==> Restored archive %s as %s' % (archivePath, _newProject))
else:
getLogger().warning('Failed to restore archive %s' % (archivePath,))
return _newProject
#-----------------------------------------------------------------------------------------
# Layouts
#-----------------------------------------------------------------------------------------
# def _getOpenLayoutPath(self):
# """Opens a saved Layout as dialog box and gets directory specified in the
# file dialog.
# :return selected path or None
# """
#
# fType = 'JSON (*.json)'
# dialog = LayoutsFileDialog(parent=self.ui.mainWindow, acceptMode='open', fileFilter=fType)
# dialog._show()
# path = dialog.selectedFile()
# if not path:
# return None
# if path:
# return path
#
# def _getSaveLayoutPath(self):
# """Opens save Layout as dialog box and gets directory specified in the
# file dialog.
# """
#
# jsonType = '.json'
# fType = 'JSON (*.json)'
# dialog = LayoutsFileDialog(parent=self.ui.mainWindow, acceptMode='save', fileFilter=fType)
# dialog._show()
# newPath = dialog.selectedFile()
# if not newPath:
# return None
#
# newPath = aPath(newPath)
# if newPath.exists():
# # should not really need to check the second and third condition above, only
# # the Qt dialog stupidly insists a directory exists before you can select it
# # so if it exists but is empty then don't bother asking the question
# title = 'Overwrite path'
# msg = 'Path "%s" already exists, continue?' % newPath
# if not MessageDialog.showYesNo(title, msg):
# return None
#
# newPath.assureSuffix(jsonType)
# return newPath
def _getUserLayout(self, userPath=None):
"""defines the application.layout dictionary.
For a saved project: uses the auto-generated during the saving process, if a user specified json file is given then
is used that one instead.
For a new project, it is used the default.
"""
# try:
if userPath:
with open(userPath) as fp:
layout = json.load(fp, object_hook=AttrDict)
self.layout = layout
else:
# opens the autogenerated if an existing project
savedLayoutPath = self._getAutogeneratedLayoutFile()
if savedLayoutPath:
with open(savedLayoutPath) as fp:
layout = json.load(fp, object_hook=AttrDict)
self.layout = layout
else: # opens the default
Layout._createLayoutFile(self)
self._getUserLayout()
# except Exception as e:
# getLogger().warning('No layout found. %s' %e)
return self.layout
# def _saveLayoutCallback(self):
# Layout.updateSavedLayout(self.ui.mainWindow)
# getLogger().info('Layout saved')
#
# def _saveLayoutAsCallback(self):
# path = self.getSaveLayoutPath()
# try:
# Layout.saveLayoutToJson(self.ui.mainWindow, jsonFilePath=path)
# getLogger().info('Layout saved')
# except Exception as es:
# getLogger().warning('Impossible to save layout. %s' % es)
# def restoreLastSavedLayout(self):
# self.ui.mainWindow.moduleArea._closeAll()
# Layout.restoreLayout(self.ui.mainWindow, self.layout, restoreSpectrumDisplay=True)
def _restoreLayoutFromFile(self, path):
if path is None:
raise ValueError('_restoreLayoutFromFile: undefined path')
try:
self._getUserLayout(path)
self.ui.mainWindow.moduleArea._closeAll()
Layout.restoreLayout(self.ui.mainWindow, self.layout, restoreSpectrumDisplay=True)
except Exception as e:
getLogger().warning('Impossible to restore layout. %s' % e)
def _getAutogeneratedLayoutFile(self):
if self.project:
layoutFile = Layout.getLayoutFile(self)
return layoutFile
###################################################################################################################
## MENU callbacks: Spectrum
###################################################################################################################
[docs] def showCopyPeaks(self):
if not self.project.peakLists:
getLogger().warning('Project has no Peak Lists. Peak Lists cannot be copied')
MessageDialog.showWarning('Project has no Peak Lists.', 'Peak Lists cannot be copied')
return
else:
from ccpn.ui.gui.popups.CopyPeaksPopup import CopyPeaks
popup = CopyPeaks(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow)
peaks = self.current.peaks
popup._selectPeaks(peaks)
popup.exec()
popup.raise_()
################################################################################################
## MENU callbacks: Molecule
################################################################################################
# @logCommand('application.')
# def toggleSequenceModule(self):
# """
# Toggles whether Sequence Module is displayed or not
# """
# self.showSequenceModule()
# @logCommand('application.')
# def showSequenceModule(self, position='top', relativeTo=None):
# """
# Displays Sequence Module at the top of the screen.
# """
# from ccpn.ui.gui.modules.SequenceModule import SequenceModule
#
# if SequenceModule._alreadyOpened is False:
# mainWindow = self.ui.mainWindow
# self.sequenceModule = SequenceModule(mainWindow=mainWindow)
# mainWindow.moduleArea.addModule(self.sequenceModule,
# position=position, relativeTo=relativeTo)
# action = self._findMenuAction('View', 'Show Sequence')
# if action:
# action.setChecked(True)
#
# # set the colours of the currently highlighted chain in open sequenceGraph
# # should really be in the class, but doesn't fire correctly during __init__
# self.sequenceModule.populateFromSequenceGraphs()
# @logCommand('application.')
# def hideSequenceModule(self):
# """Hides sequence module"""
#
# if hasattr(self, 'sequenceModule'):
# self.sequenceModule.close()
# delattr(self, 'sequenceModule')
[docs] def inspectMolecule(self):
pass
[docs] @logCommand('application.')
def showReferenceChemicalShifts(self, position='left', relativeTo=None):
"""Displays Reference Chemical Shifts module."""
from ccpn.ui.gui.modules.ReferenceChemicalShifts import ReferenceChemicalShifts
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
refChemShifts = ReferenceChemicalShifts(mainWindow=mainWindow)
mainWindow.moduleArea.addModule(refChemShifts, position=position, relativeTo=relativeTo)
return refChemShifts
###################################################################################################################
## MENU callbacks: VIEW
###################################################################################################################
[docs] @logCommand('application.')
def showChemicalShiftTable(self,
position: str = 'bottom',
relativeTo: CcpnModule = None,
chemicalShiftList=None, selectFirstItem=False):
"""Displays Chemical Shift table.
"""
from ccpn.ui.gui.modules.ChemicalShiftTable import ChemicalShiftTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
chemicalShiftTableModule = ChemicalShiftTableModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(chemicalShiftTableModule, position=position, relativeTo=relativeTo)
if chemicalShiftList:
chemicalShiftTableModule._selectTable(chemicalShiftList)
return chemicalShiftTableModule
[docs] @logCommand('application.')
def showNmrResidueTable(self, position='bottom', relativeTo=None,
nmrChain=None, selectFirstItem=False):
"""Displays Nmr Residue Table
"""
from ccpn.ui.gui.modules.NmrResidueTable import NmrResidueTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
nmrResidueTableModule = NmrResidueTableModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(nmrResidueTableModule, position=position, relativeTo=relativeTo)
if nmrChain:
nmrResidueTableModule.selectNmrChain(nmrChain)
return nmrResidueTableModule
[docs] @logCommand('application.')
def showResidueTable(self, position='bottom', relativeTo=None,
chain=None, selectFirstItem=False):
"""Displays Residue Table
"""
from ccpn.ui.gui.modules.ResidueTable import ResidueTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
residueTableModule = ResidueTableModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(residueTableModule, position=position, relativeTo=relativeTo)
if chain:
residueTableModule.selectChain(chain)
return residueTableModule
[docs] @logCommand('application.')
def showPeakTable(self, position: str = 'left', relativeTo: CcpnModule = None,
peakList: PeakList = None, selectFirstItem=False):
"""Displays Peak table on left of main window with specified list selected.
"""
from ccpn.ui.gui.modules.PeakTable import PeakTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
peakTableModule = PeakTableModule(mainWindow, selectFirstItem=selectFirstItem)
if peakList:
peakTableModule.selectPeakList(peakList)
mainWindow.moduleArea.addModule(peakTableModule, position=position, relativeTo=relativeTo)
return peakTableModule
[docs] @logCommand('application.')
def showMultipletTable(self, position: str = 'left', relativeTo: CcpnModule = None,
multipletList: MultipletList = None, selectFirstItem=False):
"""Displays multipletList table on left of main window with specified list selected.
"""
from ccpn.ui.gui.modules.MultipletListTable import MultipletTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
multipletTableModule = MultipletTableModule(mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(multipletTableModule, position=position, relativeTo=relativeTo)
if multipletList:
multipletTableModule.selectMultipletList(multipletList)
return multipletTableModule
[docs] @logCommand('application.')
def showIntegralTable(self, position: str = 'left', relativeTo: CcpnModule = None,
integralList: IntegralList = None, selectFirstItem=False):
"""Displays integral table on left of main window with specified list selected.
"""
from ccpn.ui.gui.modules.IntegralTable import IntegralTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
integralTableModule = IntegralTableModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(integralTableModule, position=position, relativeTo=relativeTo)
if integralList:
integralTableModule.selectIntegralList(integralList)
return integralTableModule
[docs] @logCommand('application.')
def showRestraintTable(self, position: str = 'bottom', relativeTo: CcpnModule = None,
restraintTable: PeakList = None, selectFirstItem=False):
"""Displays Peak table on left of main window with specified list selected.
"""
from ccpn.ui.gui.modules.RestraintTableModule import RestraintTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
restraintTableModule = RestraintTableModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(restraintTableModule, position=position, relativeTo=relativeTo)
if restraintTable:
restraintTableModule.selectRestraintTable(restraintTable)
return restraintTableModule
[docs] @logCommand('application.')
def showStructureTable(self, position='bottom', relativeTo=None,
structureEnsemble=None, selectFirstItem=False):
"""Displays Structure Table
"""
from ccpn.ui.gui.modules.StructureTable import StructureTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
structureTableModule = StructureTableModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(structureTableModule, position=position, relativeTo=relativeTo)
if structureEnsemble:
structureTableModule.selectStructureEnsemble(structureEnsemble)
return structureTableModule
[docs] @logCommand('application.')
def showDataTable(self, position='bottom', relativeTo=None,
dataTable=None, selectFirstItem=False):
"""Displays DataTable Table
"""
# from ccpn.ui.gui.modules.DataTableModuleABC import DataTableModuleBC as _module
from ccpn.ui.gui.modules.DataTableModule import DataTableModule as _module
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
if dataTable:
# _dataTableModule = DataTableModuleBC(dataTable, name=dataTable.name, mainWindow=mainWindow)
_dataTableModule = _module(mainWindow=mainWindow, table=dataTable)
mainWindow.moduleArea.addModule(_dataTableModule, position=position, relativeTo=relativeTo)
return _dataTableModule
[docs] @logCommand('application.')
def showViolationTable(self, position: str = 'bottom', relativeTo: CcpnModule = None,
violationTable: PeakList = None, selectFirstItem=False):
"""Displays Peak table on left of main window with specified list selected.
"""
from ccpn.ui.gui.modules.ViolationTableModule import ViolationTableModule as _module
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
if violationTable:
_violationTableModule = _module(mainWindow=mainWindow, table=violationTable)
mainWindow.moduleArea.addModule(_violationTableModule, position=position, relativeTo=relativeTo)
return _violationTableModule
[docs] @logCommand('application.')
def showCollectionModule(self, position='bottom', relativeTo=None,
collection=None, selectFirstItem=False):
"""Displays Collection Module
"""
pass
[docs] @logCommand('application.')
def showNotesEditor(self, position: str = 'bottom', relativeTo: CcpnModule = None,
note=None, selectFirstItem=False):
"""Displays Notes Editing Table
"""
from ccpn.ui.gui.modules.NotesEditor import NotesEditorModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
notesEditorModule = NotesEditorModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(notesEditorModule, position=position, relativeTo=relativeTo)
if note:
notesEditorModule.selectNote(note)
return notesEditorModule
[docs] @logCommand('application.')
def showRestraintAnalysisTable(self,
position: str = 'bottom',
relativeTo: CcpnModule = None,
peakList=None, selectFirstItem=False):
"""Displays restraint analysis table.
"""
from ccpn.ui.gui.modules.RestraintAnalysisTable import RestraintAnalysisTableModule
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
restraintAnalysisTableModule = RestraintAnalysisTableModule(mainWindow=mainWindow, selectFirstItem=selectFirstItem)
mainWindow.moduleArea.addModule(restraintAnalysisTableModule, position=position, relativeTo=relativeTo)
if peakList:
restraintAnalysisTableModule.selectPeakList(peakList)
return restraintAnalysisTableModule
[docs] def togglePhaseConsole(self):
if self.current.strip is not None:
self.current.strip.spectrumDisplay.togglePhaseConsole()
else:
getLogger().warning('No strip selected')
def _setZoomPopup(self):
if self.current.strip is not None:
self.current.strip._setZoomPopup()
else:
getLogger().warning('No strip selected')
[docs] def resetZoom(self):
if self.current.strip is not None:
self.current.strip.resetZoom()
else:
getLogger().warning('No strip selected')
[docs] def copyStrip(self):
if self.current.strip is not None:
self.current.strip.copyStrip()
else:
getLogger().warning('No strip selected')
def _flipXYAxisCallback(self):
"""Callback to flip axes"""
if self.current.strip is not None:
self.current.strip.flipXYAxis()
else:
getLogger().warning('No strip selected')
def _flipXZAxisCallback(self):
"""Callback to flip axes"""
if self.current.strip is not None:
self.current.strip.flipXZAxis()
else:
getLogger().warning('No strip selected')
def _flipYZAxisCallback(self):
"""Callback to flip axes"""
if self.current.strip is not None:
self.current.strip.flipYZAxis()
else:
getLogger().warning('No strip selected')
def _toggleConsoleCallback(self):
"""Toggles whether python console is displayed at bottom of the main window.
"""
self.ui.mainWindow.toggleConsole()
[docs] def showChemicalShiftMapping(self, position: str = 'top', relativeTo: CcpnModule = None):
from ccpn.ui.gui.modules.ChemicalShiftsMappingModule import ChemicalShiftsMapping
mainWindow = self.ui.mainWindow
if not relativeTo:
relativeTo = mainWindow.moduleArea
cs = ChemicalShiftsMapping(mainWindow=mainWindow)
mainWindow.moduleArea.addModule(cs, position=position, relativeTo=relativeTo)
return cs
#################################################################################################
## MENU callbacks: Macro
#################################################################################################
@logCommand('application.')
def _showMacroEditorCallback(self):
"""Displays macro editor. Just handing down to MainWindow for now
"""
self.mainWindow.newMacroEditor()
def _openMacroCallback(self, directory=None):
""" Select macro file and on MacroEditor.
"""
mainWindow = self.ui.mainWindow
dialog = MacrosFileDialog(parent=mainWindow, acceptMode='open', fileFilter='*.py', directory=directory)
dialog._show()
path = dialog.selectedFile()
if path is not None:
self.mainWindow.newMacroEditor(path=path)
[docs] def defineUserShortcuts(self):
from ccpn.ui.gui.popups.ShortcutsPopup import ShortcutsPopup
ShortcutsPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow).exec_()
[docs] def runMacro(self, macroFile: str = None):
"""
Runs a macro if a macro is specified, or opens a dialog box for selection of a macro file and then
runs the selected macro.
"""
if macroFile is None:
fType = '*.py'
dialog = MacrosFileDialog(parent=self.ui.mainWindow, acceptMode='run', fileFilter=fType)
dialog._show()
macroFile = dialog.selectedFile()
if not macroFile:
return
if not macroFile in self.preferences.recentMacros:
self.preferences.recentMacros.append(macroFile)
self.ui.mainWindow.pythonConsole._runMacro(macroFile)
#################################################################################################
def _systemOpen(self, path):
"""Open path to pdf file on system
"""
if isWindowsOS():
os.startfile(path)
elif isMacOS():
subprocess.run(['open', path], check=True)
else:
linuxCommand = self.preferences.externalPrograms.PDFViewer
# assume a linux and use the choice given in the preferences
if linuxCommand and Path.aPath(linuxCommand).is_file():
from ccpn.framework.PathsAndUrls import ccpnRunTerminal
try:
# NOTE:ED - this could be quite nasty, but can't think of another way to get Linux to open a pdf
subprocess.run([ccpnRunTerminal, linuxCommand, path])
except Exception as es:
getLogger().warning(f'Error opening PDFViewer. {es}')
MessageDialog.showWarning('Open File',
f'Error opening PDFViewer. {es}\n'
f'Check settings in Preferences->External Programs'
)
else:
# raise TypeError('PDFViewer not defined for linux')
MessageDialog.showWarning('Open File',
'Please select PDFViewer in Preferences->External Programs')
def __str__(self):
return '<%s version:%s>' % (self.applicationName, self.applicationVersion)
__repr__ = __str__
#-----------------------------------------------------------------------------------------
#end class
#-----------------------------------------------------------------------------------------
#-----------------------------------------------------------------------------------------
# code for testing purposes
#-----------------------------------------------------------------------------------------
[docs]class MyProgramme(Framework):
"""My first app"""
applicationName = 'CcpNmr'
applicationVersion = Version.applicationVersion
[docs]def createFramework(projectPath=None, **kwds):
args = Arguments(projectPath=projectPath, **kwds)
result = MyProgramme(args)
result._startApplication()
#
return result
[docs]def testMain():
_makeMainWindowVisible = False
myArgs = Arguments()
myArgs.noGui = False
myArgs.debug = True
application = MyProgramme(args=myArgs)
ui = application.ui
ui.initialize(ui.mainWindow) # ui.mainWindow not needed for refactored?
if _makeMainWindowVisible:
ui.mainWindow._updateMainWindow(newProject=True)
ui.mainWindow.show()
QtWidgets.QApplication.setActiveWindow(ui.mainWindow)
# register the programme
from ccpn.framework.Application import ApplicationContainer
container = ApplicationContainer()
container.register(application)
application.useFileLogger = True
if __name__ == '__main__':
testMain()