Source code for ccpn.framework.Framework

#=========================================================================================
# 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 showSpectrumGroupsPopup(self): if not self.project.spectra: getLogger().warning('Project has no Specta. Spectrum groups cannot be displayed') MessageDialog.showWarning('Project contains no spectra.', 'Spectrum groups cannot be displayed') else: from ccpn.ui.gui.popups.SpectrumGroupEditor import SpectrumGroupEditor if not self.project.spectrumGroups: #GST This seems to have problems MessageDialog wraps it which looks bad... # MessageDialog.showWarning('Project has no Spectrum Groups.', # 'Create them using:\nSidebar → SpectrumGroups → <New SpectrumGroup>\n ') SpectrumGroupEditor(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow, editMode=False).exec_() else: SpectrumGroupEditor(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow, editMode=True, obj=self.project.spectrumGroups[0]).exec_()
[docs] def showProjectionPopup(self): if not self.project.spectra: getLogger().warning('Project has no Specta. Make Projection Popup cannot be displayed') MessageDialog.showWarning('Project contains no spectra.', 'Make Projection Popup cannot be displayed') else: from ccpn.ui.gui.popups.SpectrumProjectionPopup import SpectrumProjectionPopup popup = SpectrumProjectionPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow) popup.exec_()
[docs] def showExperimentTypePopup(self): """ Displays experiment type popup. """ if not self.project.spectra: getLogger().warning('Experiment Type Selection: Project has no Specta.') MessageDialog.showWarning('Experiment Type Selection', 'Project has no Spectra.') else: from ccpn.ui.gui.popups.ExperimentTypePopup import ExperimentTypePopup popup = ExperimentTypePopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow) popup.exec_()
[docs] def showValidateSpectraPopup(self, spectra=None, defaultSelected=None): """ Displays validate spectra popup. """ if not self.project.spectra: getLogger().warning('Validate Spectrum Paths Selection: Project has no Specta.') MessageDialog.showWarning('Validate Spectrum Paths Selection', 'Project has no Spectra.') else: from ccpn.ui.gui.popups.ValidateSpectraPopup import ValidateSpectraPopup popup = ValidateSpectraPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow, spectra=spectra, defaultSelected=defaultSelected) popup.exec_()
[docs] def showPeakPick1DPopup(self): """ Displays Peak Picking 1D Popup. """ if not self.project.peakLists: getLogger().warning('Peak Picking: Project has no peakLists.') MessageDialog.showWarning('Peak Picking', 'Project has no peakLists.') else: spectra = [spec for spec in self.project.spectra if spec.dimensionCount == 1] if spectra: from ccpn.ui.gui.popups.PickPeaks1DPopup import PickPeak1DPopup popup = PickPeak1DPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow) popup.exec_() else: getLogger().warning('Peak Picking: Project has no 1d Specta.') MessageDialog.showWarning('Peak Picking', 'Project has no 1d Spectra.')
[docs] def showPeakPickNDPopup(self): """ Displays Peak Picking ND Popup. """ if not self.project.peakLists: getLogger().warning('Peak Picking: Project has no peakLists.') MessageDialog.showWarning('Peak Picking', 'Project has no peakLists.') else: spectra = [spec for spec in self.project.spectra if spec.dimensionCount > 1] if spectra: from ccpn.ui.gui.popups.PeakFind import PeakFindPopup popup = PeakFindPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow) popup.exec_() else: getLogger().warning('Peak Picking: Project has no Nd Specta.') MessageDialog.showWarning('Peak Picking', 'Project has no Nd Spectra.')
[docs] def showCopyPeakListPopup(self): if not self.project.peakLists: txt = 'Project has no PeakList\'s. Peak Lists cannot be copied' getLogger().warning(txt) MessageDialog.showWarning(txt) return else: from ccpn.ui.gui.popups.CopyPeakListPopup import CopyPeakListPopup popup = CopyPeakListPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow) popup.exec_()
[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_()
[docs] def showEstimateVolumesPopup(self): """ Displays Estimate Volumes Popup. """ if not self.project.peakLists: getLogger().warning('Estimate Volumes: Project has no peakLists.') MessageDialog.showWarning('Estimate Volumes', 'Project has no peakLists.') else: from ccpn.ui.gui.popups.EstimateVolumes import EstimateVolumes if self.current.strip and not self.current.strip.isDeleted: spectra = [specView.spectrum for specView in self.current.strip.spectrumDisplay.spectrumViews] else: spectra = self.project.spectra if spectra: popup = EstimateVolumes(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow, spectra=spectra) popup.exec_() else: getLogger().warning('Peak Picking: no specta selected.') MessageDialog.showWarning('Peak Picking', 'no specta selected.')
[docs] def makeStripPlotPopup(self, includePeakLists=True, includeNmrChains=True, includeNmrChainPullSelection=True): if not self.project.peaks and not self.project.nmrResidues and not self.project.nmrChains: getLogger().warning('Cannot make strip plot, nothing to display') MessageDialog.showWarning('Cannot make strip plot,', 'nothing to display') return else: if len(self.project.spectrumDisplays) == 0: MessageDialog.showWarning('', 'No SpectrumDisplay found') elif self.current.strip and not self.current.strip.isDeleted: from ccpn.ui.gui.popups.StripPlotPopup import StripPlotPopup popup = StripPlotPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow, spectrumDisplay=self.current.strip.spectrumDisplay, includePeakLists=includePeakLists, includeNmrChains=includeNmrChains, includeNmrChainPullSelection=includeNmrChainPullSelection, includeSpectrumTable=False) popup.exec_()
################################################################################################ ## MENU callbacks: Molecule ################################################################################################
[docs] @logCommand('application.') def showCreateChainPopup(self): """ Displays sequence creation popup. """ from ccpn.ui.gui.popups.CreateChainPopup import CreateChainPopup popup = CreateChainPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow) popup.exec_()
# @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 showResidueInformation(self, position: str = 'bottom', relativeTo: CcpnModule = None): """Displays Residue Information module. """ from ccpn.ui.gui.modules.ResidueInformation import ResidueInformation if not self.project.residues: getLogger().warning('No Residues in project. Residue Information Module requires Residues in the project to launch.') MessageDialog.showWarning('No Residues in project.', 'Residue Information Module requires Residues in the project to launch.') return mainWindow = self.ui.mainWindow if not relativeTo: relativeTo = mainWindow.moduleArea # ejb residueModule = ResidueInformation(mainWindow=mainWindow) mainWindow.moduleArea.addModule(residueModule, position=position, relativeTo=relativeTo) return residueModule
[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 showPrintSpectrumDisplayPopup(self): """Show the print spectrumDisplay dialog """ from ccpn.ui.gui.popups.ExportStripToFile import ExportStripToFilePopup if len(self.project.spectrumDisplays) == 0: MessageDialog.showWarning('', 'No SpectrumDisplay found') else: exportDialog = ExportStripToFilePopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow, strips=self.project.strips, selectedStrip=self.current.strip ) exportDialog.exec_()
[docs] def toggleToolbar(self): if self.current.strip is not None: self.current.strip.spectrumDisplay.toggleToolbar() else: getLogger().warning('No strip selected')
[docs] def toggleSpectrumToolbar(self): if self.current.strip is not None: self.current.strip.spectrumDisplay.toggleSpectrumToolbar() else: getLogger().warning('No strip selected')
[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')
[docs] def showFlipArbitraryAxisPopup(self): if self.current.strip is not None: if self.current.strip.spectrumDisplay.is1D: getLogger().warning('Function not permitted on 1D spectra') else: from ccpn.ui.gui.popups.CopyStripFlippedAxesPopup import CopyStripFlippedSpectraPopup popup = CopyStripFlippedSpectraPopup(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow, strip=self.current.strip, label=self.current.strip.id) popup.exec_() else: getLogger().warning('No strip selected')
[docs] def showReorderPeakListAxesPopup(self): """ Displays Reorder PeakList Axes Popup. """ if not self.project.peakLists: getLogger().warning('Reorder PeakList Axes: Project has no peakLists.') MessageDialog.showWarning('Reorder PeakList Axes', 'Project has no peakLists.') else: from ccpn.ui.gui.popups.ReorderPeakListAxes import ReorderPeakListAxes popup = ReorderPeakListAxes(parent=self.ui.mainWindow, mainWindow=self.ui.mainWindow) popup.exec_()
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()