def updatepppDisplay(self):
     for pppTab in list(self.pppCodeEdits.values()):
         self.sourceTabs.removeTab( self.sourceTabs.indexOf(pppTab) )
     self.pppCodeEdits = dict()
     if self.currentContext.pulseProgramMode == 'ppp':
         for name, text in [(self.pppSourceFile, self.pppSource)]:
             textEdit = PulseProgramSourceEdit(mode='ppp')
             encodingStrings = [encoding for encoding in EncodingDict.keys() if type(encoding) == str]
             textEdit.setupUi(textEdit, extraKeywords1=self.definitionWords+encodingStrings, extraKeywords2=self.builtinWords)
             textEdit.setPlainText(text)
             self.pppCodeEdits[name] = textEdit
             self.sourceTabs.addTab( textEdit, name )
Exemple #2
0
 def updateppDisplay(self):
     for pppTab in list(self.sourceCodeEdits.values()):
         self.sourceTabs.removeTab(self.sourceTabs.indexOf(pppTab))
     self.sourceCodeEdits = dict()
     for name, text in self.pulseProgram.source.items():
         textEdit = PulseProgramSourceEdit()
         textEdit.setupUi(textEdit,
                          extraKeywords1=self.definitionWords,
                          extraKeywords2=[key for key in OPS])
         textEdit.setPlainText(text)
         self.sourceCodeEdits[name] = textEdit
         self.sourceTabs.addTab(textEdit, name)
         textEdit.setReadOnly(self.currentContext.pulseProgramMode != 'pp')
 def updateppDisplay(self):
     for pppTab in list(self.sourceCodeEdits.values()):
         self.sourceTabs.removeTab( self.sourceTabs.indexOf(pppTab) )
     self.sourceCodeEdits = dict()
     for name, text in self.pulseProgram.source.items():
         textEdit = PulseProgramSourceEdit()
         textEdit.setupUi(textEdit, extraKeywords1=self.definitionWords, extraKeywords2=[key for key in OPS])
         textEdit.setPlainText(text)
         self.sourceCodeEdits[name] = textEdit
         self.sourceTabs.addTab( textEdit, name )
         textEdit.setReadOnly( self.currentContext.pulseProgramMode!='pp' )
Exemple #4
0
 def updatepppDisplay(self):
     for pppTab in list(self.pppCodeEdits.values()):
         self.sourceTabs.removeTab(self.sourceTabs.indexOf(pppTab))
     self.pppCodeEdits = dict()
     if self.currentContext.pulseProgramMode == 'ppp':
         for name, text in [(self.pppSourceFile, self.pppSource)]:
             textEdit = PulseProgramSourceEdit(mode='ppp')
             encodingStrings = [
                 encoding for encoding in EncodingDict.keys()
                 if type(encoding) == str
             ]
             textEdit.setupUi(textEdit,
                              extraKeywords1=self.definitionWords +
                              encodingStrings,
                              extraKeywords2=self.builtinWords)
             textEdit.setPlainText(text)
             self.pppCodeEdits[name] = textEdit
             self.sourceTabs.addTab(textEdit, name)
Exemple #5
0
    def setupUi(self, parent):
        super(ScriptingUi, self).setupUi(parent)
        self.configname = 'Scripting'
        
        #setup console
        self.consoleMaximumLines = self.config.get(self.configname+'.consoleMaximumLinesNew', 100)
        self.consoleEnable = self.config.get(self.configname+'.consoleEnable', True)
        self.consoleClearButton.clicked.connect( self.onClearConsole )
        self.linesSpinBox.valueChanged.connect( self.onConsoleMaximumLinesChanged )
        self.linesSpinBox.setValue( self.consoleMaximumLines )
        self.checkBoxEnableConsole.stateChanged.connect( self.onEnableConsole )
        self.checkBoxEnableConsole.setChecked( self.consoleEnable )
        
        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit, extraKeywords1=[], extraKeywords2=scriptFunctions)
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(QsciScintilla.Background, self.textEdit.textEdit.currentLineMarkerNum) #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(QtGui.QColor(0xd0, 0xff, 0xd0), self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)
        
        #setup documentation list
        self.getDocs()
        self.docTreeWidget.setHeaderLabels(['Available Script Functions'])
        for funcDef, funcDesc in list(self.docDict.items()):
            itemDef  = QtWidgets.QTreeWidgetItem(self.docTreeWidget, [funcDef])
            self.docTreeWidget.addTopLevelItem(itemDef)
            QtWidgets.QTreeWidgetItem(itemDef, [funcDesc])
            self.docTreeWidget.setWordWrap(True)

        #load file
        self.script.fullname = self.config.get( self.configname+'.script.fullname', '' )
        if self.script.fullname != '' and os.path.exists(self.script.fullname):
            with open(self.script.fullname, "r") as f:
                self.script.code = f.read()
        else:
            self.script.code = ''
        
        #setup filename combo box
        self.recentFiles = self.config.get( self.configname+'.recentFiles', dict() )
        self.recentFiles = {k: v for k,v in self.recentFiles.items() if os.path.exists(v)} #removes files from dict if file paths no longer exist
        self.filenameComboBox.setInsertPolicy(1)
        self.filenameComboBox.setMaxCount(10)
        self.filenameComboBox.addItems( [shortname for shortname, fullname in list(self.recentFiles.items()) if os.path.exists(fullname)] )
        self.filenameComboBox.currentIndexChanged[str].connect( self.onFilenameChange )
        self.removeCurrent.clicked.connect( self.onRemoveCurrent )
        self.filenameComboBox.setValidator( QtGui.QRegExpValidator() ) #verifies that files typed into combo box can be used
        self.updateValidator()

        #connect buttons
        self.script.repeat = self.config.get(self.configname+'.repeat',False)
        self.repeatButton.setChecked(self.script.repeat)
        self.repeatButton.clicked.connect( self.onRepeat )
        self.script.slow = self.config.get(self.configname+'.slow',False)
        self.slowButton.setChecked(self.script.slow)
        self.slowButton.clicked.connect( self.onSlow )
        self.revert = self.config.get(self.configname+'.revert',False)
        self.revertButton.setChecked(self.revert)
        self.revertButton.clicked.connect( self.onRevert )
        #File control actions
        self.actionOpen.triggered.connect( self.onLoad )
        self.actionSave.triggered.connect( self.onSave )
        self.actionReset.triggered.connect(self.onReset)
        self.actionNew.triggered.connect( self.onNew )
        #Script control actions
        self.actionStartScript.triggered.connect( self.onStartScript )
        self.actionPauseScript.triggered.connect( self.onPauseScript )
        self.actionStopScript.triggered.connect( self.onStopScript )
        self.actionPauseScriptAndScan.triggered.connect( self.onPauseScriptAndScan )
        self.actionStopScriptAndScan.triggered.connect( self.onStopScriptAndScan )
        #Script finished signal
        self.script.finished.connect( self.onFinished )

        self.loadFile(self.script.fullname)
        self.populateTree() #populates file explorer tree widget

        #Connect buttons for fileTreeWidget
        self.fileTreeWidget.itemDoubleClicked.connect(self.onDoubleClick)

        self.expandTree = QtWidgets.QAction("Expand All", self)
        self.collapseTree = QtWidgets.QAction("Collapse All", self)
        self.expandChild = QtWidgets.QAction("Expand Selected", self)
        self.collapseChild = QtWidgets.QAction("Collapse Selected", self)
        self.expandTree.triggered.connect(partial(self.onExpandOrCollapse, True, True))
        self.collapseTree.triggered.connect(partial(self.onExpandOrCollapse, True, False))
        self.expandChild.triggered.connect(partial(self.onExpandOrCollapse, False, True))
        self.collapseChild.triggered.connect(partial(self.onExpandOrCollapse, False, False))
        self.fileTreeWidget.addAction(self.expandTree)
        self.fileTreeWidget.addAction(self.collapseTree)
        self.fileTreeWidget.addAction(self.expandChild)
        self.fileTreeWidget.addAction(self.collapseChild)

        self.fileTreeWidget.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/other/icons/Terminal-icon.png"))
        self.statusLabel.setText("Idle")
Exemple #6
0
class ScriptingUi(ScriptingWidget, ScriptingBase):
    """Ui for the scripting interface."""
    def __init__(self, experimentUi):
        ScriptingWidget.__init__(self)
        ScriptingBase.__init__(self)
        self.config = experimentUi.config
        self.experimentUi = experimentUi
        self.recentFiles = dict() #dict of form {shortname: fullname}, where fullname has path and shortname doesn't
        self.script = Script() #encapsulates the script
        self.scriptHandler = ScriptHandler(self.script, experimentUi) #handles interface to the script
        self.revert = False
        self.initcode = ''
        self.defaultDir = getProject().configDir+'/Scripts'
        if not os.path.exists(self.defaultDir):
            defaultScriptsDir = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'config/Scripts')) #/IonControl/config/Scripts directory
            shutil.copytree(defaultScriptsDir, self.defaultDir) #Copy over all example scripts


    def setupUi(self, parent):
        super(ScriptingUi, self).setupUi(parent)
        self.configname = 'Scripting'
        
        #setup console
        self.consoleMaximumLines = self.config.get(self.configname+'.consoleMaximumLinesNew', 100)
        self.consoleEnable = self.config.get(self.configname+'.consoleEnable', True)
        self.consoleClearButton.clicked.connect( self.onClearConsole )
        self.linesSpinBox.valueChanged.connect( self.onConsoleMaximumLinesChanged )
        self.linesSpinBox.setValue( self.consoleMaximumLines )
        self.checkBoxEnableConsole.stateChanged.connect( self.onEnableConsole )
        self.checkBoxEnableConsole.setChecked( self.consoleEnable )
        
        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit, extraKeywords1=[], extraKeywords2=scriptFunctions)
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(QsciScintilla.Background, self.textEdit.textEdit.currentLineMarkerNum) #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(QtGui.QColor(0xd0, 0xff, 0xd0), self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)
        
        #setup documentation list
        self.getDocs()
        self.docTreeWidget.setHeaderLabels(['Available Script Functions'])
        for funcDef, funcDesc in list(self.docDict.items()):
            itemDef  = QtWidgets.QTreeWidgetItem(self.docTreeWidget, [funcDef])
            self.docTreeWidget.addTopLevelItem(itemDef)
            QtWidgets.QTreeWidgetItem(itemDef, [funcDesc])
            self.docTreeWidget.setWordWrap(True)

        #load file
        self.script.fullname = self.config.get( self.configname+'.script.fullname', '' )
        if self.script.fullname != '' and os.path.exists(self.script.fullname):
            with open(self.script.fullname, "r") as f:
                self.script.code = f.read()
        else:
            self.script.code = ''
        
        #setup filename combo box
        self.recentFiles = self.config.get( self.configname+'.recentFiles', dict() )
        self.recentFiles = {k: v for k,v in self.recentFiles.items() if os.path.exists(v)} #removes files from dict if file paths no longer exist
        self.filenameComboBox.setInsertPolicy(1)
        self.filenameComboBox.setMaxCount(10)
        self.filenameComboBox.addItems( [shortname for shortname, fullname in list(self.recentFiles.items()) if os.path.exists(fullname)] )
        self.filenameComboBox.currentIndexChanged[str].connect( self.onFilenameChange )
        self.removeCurrent.clicked.connect( self.onRemoveCurrent )
        self.filenameComboBox.setValidator( QtGui.QRegExpValidator() ) #verifies that files typed into combo box can be used
        self.updateValidator()

        #connect buttons
        self.script.repeat = self.config.get(self.configname+'.repeat',False)
        self.repeatButton.setChecked(self.script.repeat)
        self.repeatButton.clicked.connect( self.onRepeat )
        self.script.slow = self.config.get(self.configname+'.slow',False)
        self.slowButton.setChecked(self.script.slow)
        self.slowButton.clicked.connect( self.onSlow )
        self.revert = self.config.get(self.configname+'.revert',False)
        self.revertButton.setChecked(self.revert)
        self.revertButton.clicked.connect( self.onRevert )
        #File control actions
        self.actionOpen.triggered.connect( self.onLoad )
        self.actionSave.triggered.connect( self.onSave )
        self.actionReset.triggered.connect(self.onReset)
        self.actionNew.triggered.connect( self.onNew )
        #Script control actions
        self.actionStartScript.triggered.connect( self.onStartScript )
        self.actionPauseScript.triggered.connect( self.onPauseScript )
        self.actionStopScript.triggered.connect( self.onStopScript )
        self.actionPauseScriptAndScan.triggered.connect( self.onPauseScriptAndScan )
        self.actionStopScriptAndScan.triggered.connect( self.onStopScriptAndScan )
        #Script finished signal
        self.script.finished.connect( self.onFinished )

        self.loadFile(self.script.fullname)
        self.populateTree() #populates file explorer tree widget

        #Connect buttons for fileTreeWidget
        self.fileTreeWidget.itemDoubleClicked.connect(self.onDoubleClick)

        self.expandTree = QtWidgets.QAction("Expand All", self)
        self.collapseTree = QtWidgets.QAction("Collapse All", self)
        self.expandChild = QtWidgets.QAction("Expand Selected", self)
        self.collapseChild = QtWidgets.QAction("Collapse Selected", self)
        self.expandTree.triggered.connect(partial(self.onExpandOrCollapse, True, True))
        self.collapseTree.triggered.connect(partial(self.onExpandOrCollapse, True, False))
        self.expandChild.triggered.connect(partial(self.onExpandOrCollapse, False, True))
        self.collapseChild.triggered.connect(partial(self.onExpandOrCollapse, False, False))
        self.fileTreeWidget.addAction(self.expandTree)
        self.fileTreeWidget.addAction(self.collapseTree)
        self.fileTreeWidget.addAction(self.expandChild)
        self.fileTreeWidget.addAction(self.collapseChild)

        self.fileTreeWidget.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/other/icons/Terminal-icon.png"))
        self.statusLabel.setText("Idle")

    @QtCore.pyqtSlot()
    def onStartScript(self):
        """Start script button is clicked"""
        if not self.script.isRunning():
            logger = logging.getLogger(__name__)
            message = "script {0} started at {1}".format(self.script.fullname, str(datetime.now()))
            logger.info(message)
            self.writeToConsole(message, color='blue')
            self.onSave()
            self.enableScriptChange(False)
            self.actionPauseScript.setChecked(False)
            self.statusLabel.setText("Script running")
            if self.revert:
                self.savedState = True
                self.saveSettingsState()
            else:
                self.savedState = False
            self.scriptHandler.onStartScript()
            
    @QtCore.pyqtSlot(bool)
    def onPauseScript(self, paused):
        """Pause script button is clicked"""
        logger = logging.getLogger(__name__)
        message = "Script is paused" if paused else "Script is unpaused"
        markerColor = QtGui.QColor("#c0c0ff") if paused else QtGui.QColor(0xd0, 0xff, 0xd0)
        self.textEdit.textEdit.setMarkerBackgroundColor(markerColor, self.textEdit.textEdit.currentLineMarkerNum)
        logger.info(message)
        self.writeToConsole(message, color='blue')
        self.actionPauseScript.setChecked(paused)
        self.scriptHandler.onPauseScript(paused)
        
    @QtCore.pyqtSlot()
    def onStopScript(self):
        """Stop script button is clicked"""
        self.actionPauseScript.setChecked(False)
        self.repeatButton.setChecked(False)
        self.scriptHandler.onStopScript()

    @QtCore.pyqtSlot()
    def onPauseScriptAndScan(self):
        """Pause script and scan button is clicked"""
        logger = logging.getLogger(__name__)
        message = "Script is paused"
        markerColor = QtGui.QColor("#c0c0ff")
        self.textEdit.textEdit.setMarkerBackgroundColor(markerColor, self.textEdit.textEdit.currentLineMarkerNum)
        logger.info(message)
        self.writeToConsole(message, color='blue')
        self.actionPauseScript.setChecked(True)
        self.scriptHandler.onPauseScriptAndScan()
        
    @QtCore.pyqtSlot()
    def onStopScriptAndScan(self):
        """Stop script and scan button is clicked"""
        self.actionPauseScript.setChecked(False)
        self.repeatButton.setChecked(False)
        self.scriptHandler.onStopScriptAndScan()
        
    @QtCore.pyqtSlot()
    def onFinished(self):
        """Runs when script thread finishes. re-enables script GUI."""
        logger = logging.getLogger(__name__)
        self.statusLabel.setText("Idle")
        message = "script {0} finished at {1}".format(self.script.fullname, str(datetime.now()))
        logger.info(message)
        self.writeToConsole(message, color='blue')
        self.textEdit.textEdit.markerDeleteAll()
        self.enableScriptChange(True)
        if self.revert and self.savedState:
            self.restoreSettingsState()
            
    @QtCore.pyqtSlot()
    def onRepeat(self):
        """Repeat button is clicked."""
        logger = logging.getLogger(__name__)
        repeat = self.repeatButton.isChecked()
        message = "Repeat is on" if repeat else "Repeat is off"
        logger.debug(message)
        self.writeToConsole(message)
        self.scriptHandler.onRepeat(repeat)

    @QtCore.pyqtSlot()
    def onRevert(self):
        """Revert button is clicked."""
        self.revert = self.revertButton.isChecked()
        logging.getLogger(__name__).debug("Revert is on" if self.revert else "Revert is off")

    @QtCore.pyqtSlot()
    def onSlow(self):
        """Slow button is clicked."""
        logger = logging.getLogger(__name__)
        slow = self.slowButton.isChecked()
        message = "Slow is on" if slow else "Slow is off"
        logger.debug(message)
        self.writeToConsole(message)
        self.scriptHandler.onSlow(slow)

    @QtCore.pyqtSlot()
    def onNew(self):
        """New button is clicked. Pop up dialog asking for new name, and create file."""
        logger = logging.getLogger(__name__)
        shortname, ok = QtWidgets.QInputDialog.getText(self, 'New script name', 'Please enter a new script name: ')
        if ok:
            shortname = str(shortname)
            shortname = shortname.replace(' ', '_') #Replace spaces with underscores
            shortname = shortname.split('.')[0] #Take only what's before the '.'
            ensurePath(self.defaultDir + '/' + shortname)
            shortname += '.py'
            fullname = self.defaultDir + '/' + shortname
            if not os.path.exists(fullname):
                try:
                    with open(fullname, 'w') as f:
                        newFileText = '#' + shortname + ' created ' + str(datetime.now()) + '\n'
                        f.write(newFileText)
                except Exception as e:
                    message = "Unable to create new file {0}: {1}".format(shortname, e)
                    logger.error(message)
                    self.onConsoleSignal(message, False)
                    return
            self.loadFile(fullname)
            self.populateTree(fullname)

    def enableScriptChange(self, enabled):
        """Enable or disable any changes to script editor"""
        color = QtGui.QColor("#ffe4e4") if enabled else QtGui.QColor('white')
        self.textEdit.textEdit.setCaretLineVisible(enabled)
        self.textEdit.textEdit.setCaretLineBackgroundColor(color)
        self.textEdit.setReadOnly(not enabled)
        self.filenameComboBox.setDisabled(not enabled)
        self.removeCurrent.setDisabled(not enabled)
        self.actionOpen.setEnabled(enabled)
        self.actionSave.setEnabled(enabled)
        self.actionReset.setEnabled(enabled)
        self.actionNew.setEnabled(enabled)
        self.actionStartScript.setEnabled(enabled)
        self.actionPauseScript.setEnabled(not enabled)
        self.actionStopScript.setEnabled(not enabled)
        self.actionPauseScriptAndScan.setEnabled(not enabled)
        self.actionStopScriptAndScan.setEnabled(not enabled)
    
    def onFilenameChange(self, shortname ):
        """A name is typed into the filename combo box."""
        shortname = str(shortname)
        logger = logging.getLogger(__name__)
        if not shortname:
            self.script.fullname=''
            self.textEdit.setPlainText('')
        elif shortname not in self.recentFiles:
            logger.info('Use "open" or "new" commands to access a file not in the drop down menu')
            self.loadFile(self.recentFiles[self.script.shortname])
        else:
            fullname = self.recentFiles[shortname]
            if os.path.isfile(fullname) and fullname != self.script.fullname:
                self.loadFile(fullname)
                if str(self.filenameComboBox.currentText())!=fullname:
                    with BlockSignals(self.filenameComboBox) as w:
                        w.setCurrentIndex( self.filenameComboBox.findText( shortname ))
    
    def onLoad(self):
        """The load button is clicked. Open file prompt for file."""
        fullname, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Open Script', self.defaultDir, 'Python scripts (*.py *.pyw)')
        if fullname!="":
            self.loadFile(fullname)
           
    def loadFile(self, fullname):
        """Load in a file."""
        logger = logging.getLogger(__name__)
        if fullname:
            self.script.fullname = fullname
            with open(fullname, "r") as f:
                self.script.code = f.read()
            self.textEdit.setPlainText(self.script.code)
            if self.script.shortname not in self.recentFiles:
                self.recentFiles[self.script.shortname] = fullname
                self.filenameComboBox.addItem(self.script.shortname)
                self.updateValidator()
            with BlockSignals(self.filenameComboBox) as w:
                ind = w.findText(self.script.shortname)
                w.removeItem(ind)
                w.insertItem(0, self.script.shortname)
                w.setCurrentIndex(0)
            logger.info('{0} loaded'.format(self.script.fullname))
            self.initcode = copy.copy(self.script.code)

    def confirmLoad(self):
        """pop up window to confirm loss of unsaved changes when loading new file"""
        reply = QtWidgets.QMessageBox.question(self, 'Message',
            "Are you sure you want to discard changes?", QtWidgets.QMessageBox.Yes |
            QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
        if reply == QtWidgets.QMessageBox.Yes:
            return True
        return False

    def onExpandOrCollapse(self, expglobal=True, expand=True):
        """For expanding/collapsing file tree, expglobal=True will expand/collapse everything and False will
           collapse/expand only selected nodes. expand=True will expand, False will collapse"""
        if expglobal:
            root = self.fileTreeWidget.invisibleRootItem()
            self.recurseExpand(root, expand)
        else:
            selected = self.fileTreeWidget.selectedItems()
            if selected:
                for child in selected:
                    child.setExpanded(expand)
                    self.recurseExpand(child, expand)

    def recurseExpand(self, node, expand=True):
        """recursively descends into tree structure below node to expand/collapse all subdirectories.
           expand=True will expand, False will collapse."""
        for childind in range(node.childCount()):
            node.child(childind).setExpanded(expand)
            self.recurseExpand(node.child(childind), expand)

    def onDoubleClick(self, *args):
        """open a file that is double clicked in file tree"""
        if self.script.code != str(self.textEdit.toPlainText()):
            if not self.confirmLoad():
               return False
        self.loadFile(args[0].path)

    def populateTree(self, newfilepath=None):
        """constructs the file tree viewer"""
        genFileTree(self.fileTreeWidget.invisibleRootItem(), Path(self.defaultDir), newfilepath)

    def onReset(self):
        """Reset action. Reset file state saved on disk."""
        if self.script.fullname:
            self.loadFile(self.script.fullname)

    def onRemoveCurrent(self):
        """Remove current button is clicked. Remove file from combo box."""
        text = str(self.filenameComboBox.currentText())
        ind = self.filenameComboBox.findText(text)
        self.filenameComboBox.setCurrentIndex(ind)
        self.filenameComboBox.removeItem(ind)
        if text in self.recentFiles:
            self.recentFiles.pop(text)
        self.updateValidator()

    def onSave(self):
        """Save action. Save file to disk, and clear any highlighted errors."""
        logger = logging.getLogger(__name__)
        self.script.code = str(self.textEdit.toPlainText())
        self.textEdit.clearHighlightError()
        if self.script.code and self.script.fullname:
            with open(self.script.fullname, 'w') as f:
                f.write(self.script.code)
                logger.info('{0} saved'.format(self.script.fullname))
    
    def saveConfig(self):
        """Save configuration."""
        self.config[self.configname+'.recentFiles'] = self.recentFiles
        self.config[self.configname+'.script.fullname'] = self.script.fullname
        self.config[self.configname+'.revert'] = self.revert
        self.config[self.configname+'.slow'] = self.script.slow
        self.config[self.configname+'.repeat'] = self.script.repeat
        self.config[self.configname+'.isVisible'] = self.isVisible()
        self.config[self.configname+'.ScriptingUi.pos'] = self.pos()
        self.config[self.configname+'.ScriptingUi.size'] = self.size()
        self.config[self.configname+".splitterHorizontal"] = self.splitterHorizontal.saveState()
        self.config[self.configname+".splitterVertical"] = self.splitterVertical.saveState()
        self.config[self.configname+'.consoleMaximumLinesNew'] = self.consoleMaximumLines
        self.config[self.configname+'.consoleEnable'] = self.consoleEnable
       
    def show(self):
        pos = self.config.get(self.configname+'.ScriptingUi.pos')
        size = self.config.get(self.configname+'.ScriptingUi.size')
        splitterHorizontalState = self.config.get(self.configname+".splitterHorizontal")
        splitterVerticalState = self.config.get(self.configname+".splitterVertical")
        if pos:
            self.move(pos)
        if size:
            self.resize(size)
        if splitterHorizontalState:
            self.splitterHorizontal.restoreState(splitterHorizontalState)
        if splitterVerticalState:
            self.splitterVertical.restoreState(splitterVerticalState)
        QtWidgets.QDialog.show(self)

    def onClose(self):
        self.saveConfig()
        self.hide()
        
    def onClearConsole(self):
        self.textEditConsole.clear()

    def onConsoleMaximumLinesChanged(self, maxlines):
        self.consoleMaximumLines = maxlines
        self.textEditConsole.document().setMaximumBlockCount(maxlines)

    def onEnableConsole(self, state):
        self.consoleEnable = state==QtCore.Qt.Checked

    def markLocation(self, lines):
        """mark a specified location""" 
        if lines:
            self.textEdit.textEdit.markerDeleteAll()
            for line in lines:
                self.textEdit.textEdit.markerAdd(line-1, self.textEdit.textEdit.ARROW_MARKER_NUM)
                self.textEdit.textEdit.markerAdd(line-1, self.textEdit.textEdit.currentLineMarkerNum)

    def markError(self, lines, message):
        """mark error at specified lines, and show message"""
        if lines != []:
            for line in lines:
                self.textEdit.highlightError(message, line)

    def writeToConsole(self, message, error=False, color=''):
        if self.consoleEnable:
            message = str(message)
            cursor = self.textEditConsole.textCursor()
            cursor.movePosition(QtGui.QTextCursor.End)
            textColor = ('red' if error else 'black') if color=='' else color
            self.textEditConsole.setUpdatesEnabled(False)
            if textColor == 'black':
                self.textEditConsole.insertPlainText(message+'\n')
            else:
                self.textEditConsole.insertHtml(str('<p><font color='+textColor+'>'+message+'</font><br></p>'))
            self.textEditConsole.setUpdatesEnabled(True)
            self.textEditConsole.setTextCursor(cursor)
            self.textEditConsole.ensureCursorVisible()

    def getDocs(self):
        """Assemble the script function documentation into a dictionary"""
        self.docDict = OrderedDict()
        for doc in scriptDocs:
            docsplit = doc.splitlines() 
            defLine = docsplit.pop(0)
            docsplit = [line.strip() for line in docsplit]
            docsplit = '\n'.join(docsplit)
            self.docDict[defLine] = docsplit

    def updateValidator(self):
        """Make the validator match the recentFiles list. Uses regExp \\b(f1|f2|f3...)\\b, where fn are filenames."""
        regExp = '\\b('
        for shortname in self.recentFiles:
            if shortname:
                regExp += shortname + '|'
        regExp = regExp[:-1] #drop last pipe symbol
        regExp += ')\\b'
        self.filenameComboBox.validator().setRegExp(QtCore.QRegExp(regExp))

    def saveSettingsState(self):
        """Save the state of the scan, evaluation, and analysis"""
        self.originalState = dict()
        self.originalState['scan'] = self.experimentUi.tabDict['Scan'].scanControlWidget.settingsName
        self.originalState['evaluation'] = self.experimentUi.tabDict['Scan'].evaluationControlWidget.settingsName
        self.originalState['analysis'] = self.experimentUi.tabDict['Scan'].analysisControlWidget.currentAnalysisName

    def restoreSettingsState(self):
        """Restore the settings to their original values"""
        for name, value in self.scriptHandler.globalVariablesRevertDict.items():
            self.experimentUi.globalVariablesUi.model.update([('Global', name, value)])
        self.experimentUi.tabDict['Scan'].scanControlWidget.loadSetting(self.originalState['scan'])
        self.experimentUi.tabDict['Scan'].evaluationControlWidget.loadSetting(self.originalState['evaluation'])
        self.experimentUi.tabDict['Scan'].analysisControlWidget.onLoadAnalysisConfiguration(self.originalState['analysis'])
Exemple #7
0
    def setupUi(self, parent):
        super(UserFunctionsEditor, self).setupUi(parent)
        self.tableModel = EvalTableModel(self.globalDict)
        self.tableView.setModel(self.tableModel)
        self.tableView.setSortingEnabled(True)  # triggers sorting
        self.delegate = MagnitudeSpinBoxDelegate(self.globalDict)
        self.tableView.setItemDelegateForColumn(1, self.delegate)
        self.addEvalRow.clicked.connect(self.onAddRow)
        self.removeEvalRow.clicked.connect(self.onRemoveRow)

        #initialize default options
        self.optionsWindow = OptionsWindow(self.config,
                                           'UserFunctionsEditorOptions')
        self.optionsWindow.setupUi(self.optionsWindow)
        self.actionOptions.triggered.connect(self.onOpenOptions)
        self.optionsWindow.OptionsChangedSignal.connect(self.updateOptions)
        self.updateOptions()
        if self.optionsWindow.defaultExpand:
            onExpandOrCollapse(self.fileTreeWidget, True, True)

        #hot keys for copy/past and sorting
        self.filter = KeyListFilter(
            [QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown])
        self.filter.keyPressed.connect(self.onReorder)
        self.tableView.installEventFilter(self.filter)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Copy), self,
                            self.copy_to_clipboard)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Paste), self,
                            self.paste_from_clipboard)

        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit,
                              extraKeywords1=[],
                              extraKeywords2=[])
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(
            QsciScintilla.Background,
            self.textEdit.textEdit.currentLineMarkerNum
        )  #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(
            QtGui.QColor(0xd0, 0xff, 0xd0),
            self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)

        #load recent files, also checks if data was saved correctly and if files still exist
        savedfiles = self.config.get(self.configname + '.recentFiles',
                                     OrderedList())
        self.initRecentFiles(savedfiles)
        self.initComboBox()

        self.tableModel.exprList = self.config.get(
            self.configname + '.evalstr',
            [ExpressionValue(None, self.globalDict)])
        if not isinstance(self.tableModel.exprList, list) or not isinstance(
                self.tableModel.exprList[0], ExpressionValue):
            self.tableModel.exprList = [ExpressionValue(None, self.globalDict)]
        self.tableModel.dataChanged.emit(QtCore.QModelIndex(),
                                         QtCore.QModelIndex())
        self.tableModel.layoutChanged.emit()
        self.tableModel.connectAllExprVals()

        #load last opened file
        self.script.fullname = self.config.get(
            self.configname + '.script.fullname', '')
        self.initLoad()

        #connect buttons
        self.actionOpen.triggered.connect(self.onLoad)
        self.actionSave.triggered.connect(self.onSave)
        self.actionNew.triggered.connect(self.onNew)

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/latex/icons/FuncIcon2.png"))
        self.statusLabel.setText("")
        self.tableModel.updateData()
Exemple #8
0
class UserFunctionsEditor(FileTreeMixin, EditorWidget, EditorBase):
    """Ui for the user function interface."""
    def __init__(self, experimentUi, globalDict):
        super().__init__()
        self.config = experimentUi.config
        self.experimentUi = experimentUi
        self.globalDict = globalDict
        self.configDirFolder = 'UserFunctions'
        self.configname = 'UserFunctionsEditor'
        self.defaultDir = Path(getProject().configDir + '/' +
                               self.configDirFolder)
        self.displayFullPathNames = True
        self.script = UserCode(
            self.displayFullPathNames,
            self.defaultDir)  #carries around code body and filepath info
        if not self.defaultDir.exists():
            defaultScriptsDir = os.path.realpath(
                os.path.join(os.path.dirname(__file__), '..',
                             'config/' + self.configDirFolder)
            )  #/IonControl/config/UserFunctions directory
            shutil.copytree(defaultScriptsDir, str(
                self.defaultDir))  #Copy over all example scripts

    def setupUi(self, parent):
        super(UserFunctionsEditor, self).setupUi(parent)
        self.tableModel = EvalTableModel(self.globalDict)
        self.tableView.setModel(self.tableModel)
        self.tableView.setSortingEnabled(True)  # triggers sorting
        self.delegate = MagnitudeSpinBoxDelegate(self.globalDict)
        self.tableView.setItemDelegateForColumn(1, self.delegate)
        self.addEvalRow.clicked.connect(self.onAddRow)
        self.removeEvalRow.clicked.connect(self.onRemoveRow)

        #initialize default options
        self.optionsWindow = OptionsWindow(self.config,
                                           'UserFunctionsEditorOptions')
        self.optionsWindow.setupUi(self.optionsWindow)
        self.actionOptions.triggered.connect(self.onOpenOptions)
        self.optionsWindow.OptionsChangedSignal.connect(self.updateOptions)
        self.updateOptions()
        if self.optionsWindow.defaultExpand:
            onExpandOrCollapse(self.fileTreeWidget, True, True)

        #hot keys for copy/past and sorting
        self.filter = KeyListFilter(
            [QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown])
        self.filter.keyPressed.connect(self.onReorder)
        self.tableView.installEventFilter(self.filter)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Copy), self,
                            self.copy_to_clipboard)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Paste), self,
                            self.paste_from_clipboard)

        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit,
                              extraKeywords1=[],
                              extraKeywords2=[])
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(
            QsciScintilla.Background,
            self.textEdit.textEdit.currentLineMarkerNum
        )  #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(
            QtGui.QColor(0xd0, 0xff, 0xd0),
            self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)

        #load recent files, also checks if data was saved correctly and if files still exist
        savedfiles = self.config.get(self.configname + '.recentFiles',
                                     OrderedList())
        self.initRecentFiles(savedfiles)
        self.initComboBox()

        self.tableModel.exprList = self.config.get(
            self.configname + '.evalstr',
            [ExpressionValue(None, self.globalDict)])
        if not isinstance(self.tableModel.exprList, list) or not isinstance(
                self.tableModel.exprList[0], ExpressionValue):
            self.tableModel.exprList = [ExpressionValue(None, self.globalDict)]
        self.tableModel.dataChanged.emit(QtCore.QModelIndex(),
                                         QtCore.QModelIndex())
        self.tableModel.layoutChanged.emit()
        self.tableModel.connectAllExprVals()

        #load last opened file
        self.script.fullname = self.config.get(
            self.configname + '.script.fullname', '')
        self.initLoad()

        #connect buttons
        self.actionOpen.triggered.connect(self.onLoad)
        self.actionSave.triggered.connect(self.onSave)
        self.actionNew.triggered.connect(self.onNew)

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/latex/icons/FuncIcon2.png"))
        self.statusLabel.setText("")
        self.tableModel.updateData()

    def onOpenOptions(self):
        self.optionsWindow.show()
        self.optionsWindow.setWindowState(QtCore.Qt.WindowActive)
        self.optionsWindow.raise_()

    def updateOptions(self):
        self.filenameComboBox.setMaxCount(self.optionsWindow.lineno)
        self.displayFullPathNames = self.optionsWindow.displayPath
        self.script.dispfull = self.optionsWindow.displayPath
        self.defaultExpandAll = self.optionsWindow.defaultExpand
        self.updateFileComboBoxNames(self.displayFullPathNames)

    @QtCore.pyqtSlot()
    def onNew(self):
        """New button is clicked. Pop up dialog asking for new name, and create file."""
        logger = logging.getLogger(__name__)
        shortname, ok = QtWidgets.QInputDialog.getText(
            self, 'New script name',
            'Enter new file name (optional path specified by localpath/filename): '
        )
        if ok:
            shortname = str(shortname)
            shortname = shortname.replace(
                ' ', '_')  #Replace spaces with underscores
            shortname = shortname.split(
                '.')[0] + '.py'  #Take only what's before the '.'
            fullname = self.defaultDir.joinpath(shortname)
            ensurePath(fullname.parent)
            if not fullname.exists():
                try:
                    with fullname.open('w') as f:
                        newFileText = '#' + shortname + ' created ' + str(
                            datetime.now()) + '\n\n'
                        f.write(newFileText)
                        defaultImportText = 'from expressionFunctions.ExprFuncDecorator import userfunc\n\n'
                        f.write(defaultImportText)
                except Exception as e:
                    message = "Unable to create new file {0}: {1}".format(
                        shortname, e)
                    logger.error(message)
                    return
            self.loadFile(fullname)
            self.populateTree(fullname)

    def onComboIndexChange(self, ind):
        """A name is typed into the filename combo box."""
        if ind == 0:
            return False
        if self.script.code != str(self.textEdit.toPlainText()):
            if not self.confirmLoad():
                self.filenameComboBox.setCurrentIndex(0)
                return False
        self.loadFile(self.filenameComboBox.itemData(ind))

    def onLoad(self):
        """The load button is clicked. Open file prompt for file."""
        fullname, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, 'Open Script', self.defaultDir,
            'Python scripts (*.py *.pyw)')
        if fullname != "":
            self.loadFile(fullname)

    def loadFile(self, fullname):
        """Load in a file."""
        logger = logging.getLogger(__name__)
        if fullname:
            self.script.fullname = fullname
            with fullname.open("r") as f:
                self.script.code = f.read()
            self.textEdit.setPlainText(self.script.code)
            if self.script.fullname not in self.recentFiles:
                self.filenameComboBox.addItem(self.script.shortname)
            self.recentFiles.add(fullname)
            with BlockSignals(self.filenameComboBox) as w:
                ind = w.findText(
                    str(self.script.shortname
                        ))  #having issues with findData Path object comparison
                w.removeItem(
                    ind
                )  #these two lines just push the loaded filename to the top of the combobox
                w.insertItem(0, str(self.script.shortname))
                w.setItemData(0, self.script.fullname)
                w.setCurrentIndex(0)
            logger.info('{0} loaded'.format(self.script.fullname))
            self.initcode = copy.copy(self.script.code)

    def onRemoveCurrent(self):
        """Remove current button is clicked. Remove file from combo box."""
        path = self.filenameComboBox.currentData()
        if path in self.recentFiles:
            self.recentFiles.remove(path)
        self.filenameComboBox.removeItem(0)
        self.loadFile(self.filenameComboBox.currentData())

    def onSave(self):
        """Save action. Save file to disk, and clear any highlighted errors."""
        logger = logging.getLogger(__name__)
        self.script.code = str(self.textEdit.toPlainText())
        self.textEdit.clearHighlightError()
        if self.script.code and self.script.fullname:
            with self.script.fullname.open('w') as f:
                f.write(self.script.code)
                logger.info('{0} saved'.format(self.script.fullname))
            self.initcode = copy.copy(self.script.code)
        try:
            importlib.machinery.SourceFileLoader(
                "UserFunctions", str(self.script.fullname)).load_module()
            self.tableModel.updateData()
            ExprFunUpdate.dataChanged.emit('__exprfunc__')
            self.statusLabel.setText("Successfully updated {0}".format(
                self.script.fullname.name))
            self.statusLabel.setStyleSheet('color: green')
        except SyntaxError as e:
            self.statusLabel.setText("Failed to execute {0}: {1}".format(
                self.script.fullname.name, e))
            self.statusLabel.setStyleSheet('color: red')

    def saveConfig(self):
        """Save configuration."""
        self.config[self.configname + '.recentFiles'] = self.recentFiles
        self.config[self.configname +
                    '.script.fullname'] = self.script.fullname
        self.config[self.configname + '.isVisible'] = self.isVisible()
        self.config[self.configname + '.ScriptingUi.pos'] = self.pos()
        self.config[self.configname + '.ScriptingUi.size'] = self.size()
        self.config[
            self.configname +
            ".splitterHorizontal"] = self.splitterHorizontal.saveState()
        self.config[self.configname +
                    ".splitterVertical"] = self.splitterVertical.saveState()
        self.config[self.configname + ".evalstr"] = self.tableModel.exprList

    def show(self):
        pos = self.config.get(self.configname + '.ScriptingUi.pos')
        size = self.config.get(self.configname + '.ScriptingUi.size')
        splitterHorizontalState = self.config.get(self.configname +
                                                  ".splitterHorizontal")
        splitterVerticalState = self.config.get(self.configname +
                                                ".splitterVertical")
        if pos:
            self.move(pos)
        if size:
            self.resize(size)
        if splitterHorizontalState:
            self.splitterHorizontal.restoreState(splitterHorizontalState)
        if splitterVerticalState:
            self.splitterVertical.restoreState(splitterVerticalState)
        QtWidgets.QDialog.show(self)

    def onAddRow(self):
        """add a row in expression tests"""
        self.tableModel.insertRow()

    def onRemoveRow(self):
        """remove row(s) in expression tests"""
        zeroColSelInd = self.tableView.selectedIndexes()
        if len(zeroColSelInd):
            initRow = zeroColSelInd[0].row()
            finRow = zeroColSelInd[-1].row() - initRow + 1
            self.tableModel.removeRows(initRow, finRow)
        else:
            self.tableModel.removeRows(len(self.tableModel.exprList) - 1)

    def copy_to_clipboard(self):
        """ Copy the list of selected rows to the clipboard as a string. """
        clip = QtWidgets.QApplication.clipboard()
        rows = sorted(
            unique([i.row() for i in self.tableView.selectedIndexes()]))
        clip.setText(str(rows))

    def paste_from_clipboard(self):
        """ Append the string of rows from the clipboard to the end of the TODO list. """
        clip = QtWidgets.QApplication.clipboard()
        row_string = str(clip.text())
        try:
            row_list = list(map(int, row_string.strip('[]').split(',')))
        except ValueError:
            raise ValueError(
                "Invalid data on clipboard. Cannot paste into eval list")
        zeroColSelInd = self.tableView.selectedIndexes()
        initRow = zeroColSelInd[-1].row()
        self.tableModel.copy_rows(row_list, initRow)

    def onReorder(self, key):
        """reorder expression tests with pgup and pgdn"""
        if key in [QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]:
            indexes = self.tableView.selectedIndexes()
            up = key == QtCore.Qt.Key_PageUp
            delta = -1 if up else 1
            rows = sorted(unique([i.row() for i in indexes]), reverse=not up)
            if self.tableModel.moveRow(rows, delta):
                selectionModel = self.tableView.selectionModel()
                selectionModel.clearSelection()
                for index in indexes:
                    selectionModel.select(
                        self.tableModel.createIndex(index.row() + delta,
                                                    index.column()),
                        QtCore.QItemSelectionModel.Select)

    def onClose(self):
        self.saveConfig()
        self.hide()
    def setupUi(self, parent):
        super(UserFunctionsEditor, self).setupUi(parent)
        self.configname = 'UserFunctionsEditor'
        self.fileTreeWidget.setHeaderLabels(['User Function Files'])
        self.populateTree()

        self.tableModel = EvalTableModel(self.globalDict)
        self.tableView.setModel(self.tableModel)
        self.tableView.setSortingEnabled(True)  # triggers sorting
        self.delegate = MagnitudeSpinBoxDelegate(self.globalDict)
        self.tableView.setItemDelegateForColumn(1, self.delegate)
        self.addEvalRow.clicked.connect(self.onAddRow)
        self.removeEvalRow.clicked.connect(self.onRemoveRow)

        #hot keys for copy/past and sorting
        self.filter = KeyListFilter(
            [QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown])
        self.filter.keyPressed.connect(self.onReorder)
        self.tableView.installEventFilter(self.filter)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Copy), self,
                            self.copy_to_clipboard)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Paste), self,
                            self.paste_from_clipboard)

        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit,
                              extraKeywords1=[],
                              extraKeywords2=[])
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(
            QsciScintilla.Background,
            self.textEdit.textEdit.currentLineMarkerNum
        )  #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(
            QtGui.QColor(0xd0, 0xff, 0xd0),
            self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)

        #load file
        self.script.fullname = self.config.get(
            self.configname + '.script.fullname', '')
        self.tableModel.exprList = self.config.get(
            self.configname + '.evalstr',
            [ExpressionValue(None, self.globalDict)])
        if not isinstance(self.tableModel.exprList, list) or not isinstance(
                self.tableModel.exprList[0], ExpressionValue):
            self.tableModel.exprList = [ExpressionValue(None, self.globalDict)]
        self.tableModel.dataChanged.emit(QtCore.QModelIndex(),
                                         QtCore.QModelIndex())
        self.tableModel.layoutChanged.emit()
        self.tableModel.connectAllExprVals()
        if self.script.fullname != '' and os.path.exists(self.script.fullname):
            with open(self.script.fullname, "r") as f:
                self.script.code = f.read()
        else:
            self.script.code = ''

        #setup filename combo box
        self.recentFiles = self.config.get(self.configname + '.recentFiles',
                                           dict())
        self.recentFiles = {
            k: v
            for k, v in self.recentFiles.items() if os.path.exists(v)
        }  #removes files from dict if file paths no longer exist
        self.filenameComboBox.setInsertPolicy(1)
        self.filenameComboBox.setMaxCount(10)
        self.filenameComboBox.addItems([
            shortname for shortname, fullname in list(self.recentFiles.items())
            if os.path.exists(fullname)
        ])
        self.filenameComboBox.currentIndexChanged[str].connect(
            self.onFilenameChange)
        self.removeCurrent.clicked.connect(self.onRemoveCurrent)
        self.filenameComboBox.setValidator(QtGui.QRegExpValidator(
        ))  #verifies that files typed into combo box can be used
        self.updateValidator()

        #connect buttons
        self.actionOpen.triggered.connect(self.onLoad)
        self.actionSave.triggered.connect(self.onSave)
        self.actionNew.triggered.connect(self.onNew)

        self.fileTreeWidget.itemDoubleClicked.connect(self.onDoubleClick)
        self.loadFile(self.script.fullname)

        self.expandTree = QtWidgets.QAction("Expand All", self)
        self.collapseTree = QtWidgets.QAction("Collapse All", self)
        self.expandChild = QtWidgets.QAction("Expand Selected", self)
        self.collapseChild = QtWidgets.QAction("Collapse Selected", self)
        self.expandTree.triggered.connect(
            partial(self.onExpandOrCollapse, True, True))
        self.collapseTree.triggered.connect(
            partial(self.onExpandOrCollapse, True, False))
        self.expandChild.triggered.connect(
            partial(self.onExpandOrCollapse, False, True))
        self.collapseChild.triggered.connect(
            partial(self.onExpandOrCollapse, False, False))
        self.fileTreeWidget.addAction(self.expandTree)
        self.fileTreeWidget.addAction(self.collapseTree)
        self.fileTreeWidget.addAction(self.expandChild)
        self.fileTreeWidget.addAction(self.collapseChild)

        self.fileTreeWidget.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/latex/icons/FuncIcon2.png"))
        self.statusLabel.setText("")
        self.tableModel.updateData()
class UserFunctionsEditor(EditorWidget, EditorBase):
    """Ui for the user function interface."""
    def __init__(self, experimentUi, globalDict):
        super().__init__()
        self.config = experimentUi.config
        self.experimentUi = experimentUi
        self.globalDict = globalDict
        self.recentFiles = dict(
        )  #dict of form {shortname: fullname}, where fullname has path and shortname doesn't
        self.script = UserCode()  #carries around code body and filepath info
        self.defaultDir = getProject().configDir + '/UserFunctions'
        if not os.path.exists(self.defaultDir):
            defaultScriptsDir = os.path.realpath(
                os.path.join(os.path.dirname(__file__), '..',
                             'config/UserFunctions')
            )  #/IonControl/config/UserFunctions directory
            shutil.copytree(defaultScriptsDir,
                            self.defaultDir)  #Copy over all example scripts

    def setupUi(self, parent):
        super(UserFunctionsEditor, self).setupUi(parent)
        self.configname = 'UserFunctionsEditor'
        self.fileTreeWidget.setHeaderLabels(['User Function Files'])
        self.populateTree()

        self.tableModel = EvalTableModel(self.globalDict)
        self.tableView.setModel(self.tableModel)
        self.tableView.setSortingEnabled(True)  # triggers sorting
        self.delegate = MagnitudeSpinBoxDelegate(self.globalDict)
        self.tableView.setItemDelegateForColumn(1, self.delegate)
        self.addEvalRow.clicked.connect(self.onAddRow)
        self.removeEvalRow.clicked.connect(self.onRemoveRow)

        #hot keys for copy/past and sorting
        self.filter = KeyListFilter(
            [QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown])
        self.filter.keyPressed.connect(self.onReorder)
        self.tableView.installEventFilter(self.filter)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Copy), self,
                            self.copy_to_clipboard)
        QtWidgets.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Paste), self,
                            self.paste_from_clipboard)

        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit,
                              extraKeywords1=[],
                              extraKeywords2=[])
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(
            QsciScintilla.Background,
            self.textEdit.textEdit.currentLineMarkerNum
        )  #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(
            QtGui.QColor(0xd0, 0xff, 0xd0),
            self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)

        #load file
        self.script.fullname = self.config.get(
            self.configname + '.script.fullname', '')
        self.tableModel.exprList = self.config.get(
            self.configname + '.evalstr',
            [ExpressionValue(None, self.globalDict)])
        if not isinstance(self.tableModel.exprList, list) or not isinstance(
                self.tableModel.exprList[0], ExpressionValue):
            self.tableModel.exprList = [ExpressionValue(None, self.globalDict)]
        self.tableModel.dataChanged.emit(QtCore.QModelIndex(),
                                         QtCore.QModelIndex())
        self.tableModel.layoutChanged.emit()
        self.tableModel.connectAllExprVals()
        if self.script.fullname != '' and os.path.exists(self.script.fullname):
            with open(self.script.fullname, "r") as f:
                self.script.code = f.read()
        else:
            self.script.code = ''

        #setup filename combo box
        self.recentFiles = self.config.get(self.configname + '.recentFiles',
                                           dict())
        self.recentFiles = {
            k: v
            for k, v in self.recentFiles.items() if os.path.exists(v)
        }  #removes files from dict if file paths no longer exist
        self.filenameComboBox.setInsertPolicy(1)
        self.filenameComboBox.setMaxCount(10)
        self.filenameComboBox.addItems([
            shortname for shortname, fullname in list(self.recentFiles.items())
            if os.path.exists(fullname)
        ])
        self.filenameComboBox.currentIndexChanged[str].connect(
            self.onFilenameChange)
        self.removeCurrent.clicked.connect(self.onRemoveCurrent)
        self.filenameComboBox.setValidator(QtGui.QRegExpValidator(
        ))  #verifies that files typed into combo box can be used
        self.updateValidator()

        #connect buttons
        self.actionOpen.triggered.connect(self.onLoad)
        self.actionSave.triggered.connect(self.onSave)
        self.actionNew.triggered.connect(self.onNew)

        self.fileTreeWidget.itemDoubleClicked.connect(self.onDoubleClick)
        self.loadFile(self.script.fullname)

        self.expandTree = QtWidgets.QAction("Expand All", self)
        self.collapseTree = QtWidgets.QAction("Collapse All", self)
        self.expandChild = QtWidgets.QAction("Expand Selected", self)
        self.collapseChild = QtWidgets.QAction("Collapse Selected", self)
        self.expandTree.triggered.connect(
            partial(self.onExpandOrCollapse, True, True))
        self.collapseTree.triggered.connect(
            partial(self.onExpandOrCollapse, True, False))
        self.expandChild.triggered.connect(
            partial(self.onExpandOrCollapse, False, True))
        self.collapseChild.triggered.connect(
            partial(self.onExpandOrCollapse, False, False))
        self.fileTreeWidget.addAction(self.expandTree)
        self.fileTreeWidget.addAction(self.collapseTree)
        self.fileTreeWidget.addAction(self.expandChild)
        self.fileTreeWidget.addAction(self.collapseChild)

        self.fileTreeWidget.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/latex/icons/FuncIcon2.png"))
        self.statusLabel.setText("")
        self.tableModel.updateData()

    def onExpandOrCollapse(self, expglobal=True, expand=True):
        """For expanding/collapsing file tree, expglobal=True will expand/collapse everything and False will
           collapse/expand only selected nodes. expand=True will expand, False will collapse"""
        if expglobal:
            root = self.fileTreeWidget.invisibleRootItem()
            self.recurseExpand(root, expand)
        else:
            selected = self.fileTreeWidget.selectedItems()
            if selected:
                for child in selected:
                    child.setExpanded(expand)
                    self.recurseExpand(child, expand)

    def recurseExpand(self, node, expand=True):
        """recursively descends into tree structure below node to expand/collapse all subdirectories.
           expand=True will expand, False will collapse."""
        for childind in range(node.childCount()):
            node.child(childind).setExpanded(expand)
            self.recurseExpand(node.child(childind), expand)

    def confirmLoad(self):
        """pop up window to confirm loss of unsaved changes when loading new file"""
        reply = QtWidgets.QMessageBox.question(
            self, 'Message', "Are you sure you want to discard changes?",
            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
            QtWidgets.QMessageBox.No)
        if reply == QtWidgets.QMessageBox.Yes:
            return True
        return False

    def onDoubleClick(self, *args):
        """open a file that is double clicked in file tree"""
        if self.script.code != str(self.textEdit.toPlainText()):
            if not self.confirmLoad():
                return False
        if not args[0].isdir:
            self.loadFile(args[0].path)

    def populateTree(self, newfilepath=None):
        """constructs the file tree viewer"""
        genFileTree(self.fileTreeWidget.invisibleRootItem(),
                    Path(self.defaultDir), newfilepath)

    @QtCore.pyqtSlot()
    def onNew(self):
        """New button is clicked. Pop up dialog asking for new name, and create file."""
        logger = logging.getLogger(__name__)
        shortname, ok = QtWidgets.QInputDialog.getText(
            self, 'New script name',
            'Enter new file name (optional path specified by localpath/filename): '
        )
        if ok:
            shortname = str(shortname)
            shortname = shortname.replace(
                ' ', '_')  #Replace spaces with underscores
            shortname = shortname.split('.')[
                0]  #Take only what's before the '.'
            ensurePath(self.defaultDir + '/' + shortname)
            shortname += '.py'
            fullname = self.defaultDir + '/' + shortname
            if not os.path.exists(fullname):
                try:
                    with open(fullname, 'w') as f:
                        newFileText = '#' + shortname + ' created ' + str(
                            datetime.now()) + '\n\n'
                        f.write(newFileText)
                        defaultImportText = 'from expressionFunctions.ExprFuncDecorator import userfunc\n\n'
                        f.write(defaultImportText)
                except Exception as e:
                    message = "Unable to create new file {0}: {1}".format(
                        shortname, e)
                    logger.error(message)
                    return
            self.loadFile(fullname)
            self.populateTree(fullname)

    def onFilenameChange(self, shortname):
        """A name is typed into the filename combo box."""
        shortname = str(shortname)
        logger = logging.getLogger(__name__)
        if not shortname:
            self.script.fullname = ''
            self.textEdit.setPlainText('')
        elif shortname not in self.recentFiles:
            logger.info(
                'Use "open" or "new" commands to access a file not in the drop down menu'
            )
            self.loadFile(self.recentFiles[self.script.shortname])
        else:
            fullname = self.recentFiles[shortname]
            if os.path.isfile(fullname) and fullname != self.script.fullname:
                self.loadFile(fullname)
                if str(self.filenameComboBox.currentText()) != fullname:
                    with BlockSignals(self.filenameComboBox) as w:
                        w.setCurrentIndex(
                            self.filenameComboBox.findText(shortname))

    def onLoad(self):
        """The load button is clicked. Open file prompt for file."""
        fullname, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, 'Open Script', self.defaultDir,
            'Python scripts (*.py *.pyw)')
        if fullname != "":
            self.loadFile(fullname)

    def loadFile(self, fullname):
        """Load in a file."""
        logger = logging.getLogger(__name__)
        if fullname:
            self.script.fullname = fullname
            with open(fullname, "r") as f:
                self.script.code = f.read()
            self.textEdit.setPlainText(self.script.code)
            if self.script.shortname not in self.recentFiles:
                self.recentFiles[self.script.shortname] = fullname
                self.filenameComboBox.addItem(self.script.shortname)
                self.updateValidator()
            with BlockSignals(self.filenameComboBox) as w:
                ind = w.findText(self.script.shortname)
                w.removeItem(ind)
                w.insertItem(0, self.script.shortname)
                w.setCurrentIndex(0)
            logger.info('{0} loaded'.format(self.script.fullname))
            self.initcode = copy.copy(self.script.code)

    def onRemoveCurrent(self):
        """Remove current button is clicked. Remove file from combo box."""
        text = str(self.filenameComboBox.currentText())
        ind = self.filenameComboBox.findText(text)
        self.filenameComboBox.setCurrentIndex(ind)
        self.filenameComboBox.removeItem(ind)
        if text in self.recentFiles:
            self.recentFiles.pop(text)
        self.updateValidator()

    def onSave(self):
        """Save action. Save file to disk, and clear any highlighted errors."""
        logger = logging.getLogger(__name__)
        self.script.code = str(self.textEdit.toPlainText())
        self.textEdit.clearHighlightError()
        if self.script.code and self.script.fullname:
            with open(self.script.fullname, 'w') as f:
                f.write(self.script.code)
                logger.info('{0} saved'.format(self.script.fullname))
            self.initcode = copy.copy(self.script.code)
        try:
            importlib.machinery.SourceFileLoader(
                "UserFunctions", self.script.fullname).load_module()
            self.tableModel.updateData()
            ExprFunUpdate.dataChanged.emit('__exprfunc__')
            self.statusLabel.setText("Successfully updated {0}".format(
                self.script.fullname.split('\\')[-1]))
            self.statusLabel.setStyleSheet('color: green')
        except SyntaxError as e:
            self.statusLabel.setText("Failed to execute {0}: {1}".format(
                self.script.fullname.split('\\')[-1], e))
            self.statusLabel.setStyleSheet('color: red')

    def saveConfig(self):
        """Save configuration."""
        self.config[self.configname + '.recentFiles'] = self.recentFiles
        self.config[self.configname +
                    '.script.fullname'] = self.script.fullname
        self.config[self.configname + '.isVisible'] = self.isVisible()
        self.config[self.configname + '.ScriptingUi.pos'] = self.pos()
        self.config[self.configname + '.ScriptingUi.size'] = self.size()
        self.config[
            self.configname +
            ".splitterHorizontal"] = self.splitterHorizontal.saveState()
        self.config[self.configname +
                    ".splitterVertical"] = self.splitterVertical.saveState()
        self.config[self.configname + ".evalstr"] = self.tableModel.exprList

    def show(self):
        pos = self.config.get(self.configname + '.ScriptingUi.pos')
        size = self.config.get(self.configname + '.ScriptingUi.size')
        splitterHorizontalState = self.config.get(self.configname +
                                                  ".splitterHorizontal")
        splitterVerticalState = self.config.get(self.configname +
                                                ".splitterVertical")
        if pos:
            self.move(pos)
        if size:
            self.resize(size)
        if splitterHorizontalState:
            self.splitterHorizontal.restoreState(splitterHorizontalState)
        if splitterVerticalState:
            self.splitterVertical.restoreState(splitterVerticalState)
        QtWidgets.QDialog.show(self)

    def onAddRow(self):
        """add a row in expression tests"""
        self.tableModel.insertRow()

    def onRemoveRow(self):
        """remove row(s) in expression tests"""
        zeroColSelInd = self.tableView.selectedIndexes()
        if len(zeroColSelInd):
            initRow = zeroColSelInd[0].row()
            finRow = zeroColSelInd[-1].row() - initRow + 1
            self.tableModel.removeRows(initRow, finRow)
        else:
            self.tableModel.removeRows(len(self.tableModel.exprList) - 1)

    def copy_to_clipboard(self):
        """ Copy the list of selected rows to the clipboard as a string. """
        clip = QtWidgets.QApplication.clipboard()
        rows = sorted(
            unique([i.row() for i in self.tableView.selectedIndexes()]))
        clip.setText(str(rows))

    def paste_from_clipboard(self):
        """ Append the string of rows from the clipboard to the end of the TODO list. """
        clip = QtWidgets.QApplication.clipboard()
        row_string = str(clip.text())
        try:
            row_list = list(map(int, row_string.strip('[]').split(',')))
        except ValueError:
            raise ValueError(
                "Invalid data on clipboard. Cannot paste into eval list")
        zeroColSelInd = self.tableView.selectedIndexes()
        initRow = zeroColSelInd[-1].row()
        self.tableModel.copy_rows(row_list, initRow)

    def onReorder(self, key):
        """reorder expression tests with pgup and pgdn"""
        if key in [QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]:
            indexes = self.tableView.selectedIndexes()
            up = key == QtCore.Qt.Key_PageUp
            delta = -1 if up else 1
            rows = sorted(unique([i.row() for i in indexes]), reverse=not up)
            if self.tableModel.moveRow(rows, delta):
                selectionModel = self.tableView.selectionModel()
                selectionModel.clearSelection()
                for index in indexes:
                    selectionModel.select(
                        self.tableModel.createIndex(index.row() + delta,
                                                    index.column()),
                        QtCore.QItemSelectionModel.Select)

    def updateValidator(self):
        """Make the validator match the recentFiles list. Uses regExp \\b(f1|f2|f3...)\\b, where fn are filenames."""
        regExp = '\\b('
        for shortname in self.recentFiles:
            if shortname:
                regExp += shortname + '|'
        regExp = regExp[:-1]  #drop last pipe symbol
        regExp += ')\\b'
        self.filenameComboBox.validator().setRegExp(QtCore.QRegExp(regExp))

    def onClose(self):
        self.saveConfig()
        self.hide()
    def setupUi(self, parent):
        super(ScriptingUi, self).setupUi(parent)
        self.configname = 'Scripting'

        #initialize default options
        self.optionsWindow = OptionsWindow(self.config, 'ScriptingEditorOptions')
        self.optionsWindow.setupUi(self.optionsWindow)
        self.actionOptions.triggered.connect(self.onOpenOptions)
        self.optionsWindow.OptionsChangedSignal.connect(self.updateOptions)
        self.updateOptions()
        if self.optionsWindow.defaultExpand:
            onExpandOrCollapse(self.fileTreeWidget, True, True)

        #setup console
        self.consoleMaximumLines = self.config.get(self.configname+'.consoleMaximumLinesNew', 100)
        self.consoleEnable = self.config.get(self.configname+'.consoleEnable', True)
        self.consoleClearButton.clicked.connect( self.onClearConsole )
        self.linesSpinBox.valueChanged.connect( self.onConsoleMaximumLinesChanged )
        self.linesSpinBox.setValue( self.consoleMaximumLines )
        self.checkBoxEnableConsole.stateChanged.connect( self.onEnableConsole )
        self.checkBoxEnableConsole.setChecked( self.consoleEnable )
        
        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit, extraKeywords1=[], extraKeywords2=scriptFunctions)
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(QsciScintilla.Background, self.textEdit.textEdit.currentLineMarkerNum) #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(QtGui.QColor(0xd0, 0xff, 0xd0), self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)
        
        #setup documentation list
        self.getDocs()
        self.docTreeWidget.setHeaderLabels(['Available Script Functions'])
        for funcDef, funcDesc in list(self.docDict.items()):
            itemDef  = QtWidgets.QTreeWidgetItem(self.docTreeWidget, [funcDef])
            self.docTreeWidget.addTopLevelItem(itemDef)
            QtWidgets.QTreeWidgetItem(itemDef, [funcDesc])
            self.docTreeWidget.setWordWrap(True)

        #load recent files, also checks if data was saved correctly and if files still exist
        savedfiles = self.config.get( self.configname+'.recentFiles', OrderedList())
        self.initRecentFiles(savedfiles)
        self.initComboBox()

        #load last opened file
        self.script.fullname = self.config.get( self.configname+'.script.fullname', '' )
        self.initLoad()

        #connect buttons
        self.script.repeat = self.config.get(self.configname+'.repeat',False)
        self.repeatButton.setChecked(self.script.repeat)
        self.repeatButton.clicked.connect( self.onRepeat )
        self.script.slow = self.config.get(self.configname+'.slow',False)
        self.slowButton.setChecked(self.script.slow)
        self.slowButton.clicked.connect( self.onSlow )
        self.revert = self.config.get(self.configname+'.revert',False)
        self.revertButton.setChecked(self.revert)
        self.revertButton.clicked.connect( self.onRevert )
        #File control actions
        self.actionOpen.triggered.connect( self.onLoad )
        self.actionSave.triggered.connect( self.onSave )
        self.actionReset.triggered.connect(self.onReset)
        self.actionNew.triggered.connect( self.onNew )
        #Script control actions
        self.actionStartScript.triggered.connect( self.onStartScript )
        self.actionPauseScript.triggered.connect( self.onPauseScript )
        self.actionStopScript.triggered.connect( self.onStopScript )
        self.actionPauseScriptAndScan.triggered.connect( self.onPauseScriptAndScan )
        self.actionStopScriptAndScan.triggered.connect( self.onStopScriptAndScan )
        #Script finished signal
        self.script.finished.connect( self.onFinished )

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/other/icons/Terminal-icon.png"))
        self.statusLabel.setText("Idle")
class ScriptingUi(FileTreeMixin, ScriptingWidget, ScriptingBase):
    """Ui for the scripting interface."""
    def __init__(self, experimentUi):
        ScriptingWidget.__init__(self)
        ScriptingBase.__init__(self)
        self.config = experimentUi.config
        self.experimentUi = experimentUi
        self.recentFiles = dict() #dict of form {shortname: fullname}, where fullname has path and shortname doesn't
        self.defaultDir = Path(getProject().configDir+'/Scripts')
        self.script = Script(homeDir=self.defaultDir) #encapsulates the script
        self.scriptHandler = ScriptHandler(self.script, experimentUi) #handles interface to the script
        self.revert = False
        self.allowFileViewerLoad = True
        self.initcode = ''
        if not self.defaultDir.exists():
            defaultScriptsDir = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'config/Scripts')) #/IonControl/config/Scripts directory
            shutil.copytree(defaultScriptsDir, str(self.defaultDir)) #Copy over all example scripts

    def setupUi(self, parent):
        super(ScriptingUi, self).setupUi(parent)
        self.configname = 'Scripting'

        #initialize default options
        self.optionsWindow = OptionsWindow(self.config, 'ScriptingEditorOptions')
        self.optionsWindow.setupUi(self.optionsWindow)
        self.actionOptions.triggered.connect(self.onOpenOptions)
        self.optionsWindow.OptionsChangedSignal.connect(self.updateOptions)
        self.updateOptions()
        if self.optionsWindow.defaultExpand:
            onExpandOrCollapse(self.fileTreeWidget, True, True)

        #setup console
        self.consoleMaximumLines = self.config.get(self.configname+'.consoleMaximumLinesNew', 100)
        self.consoleEnable = self.config.get(self.configname+'.consoleEnable', True)
        self.consoleClearButton.clicked.connect( self.onClearConsole )
        self.linesSpinBox.valueChanged.connect( self.onConsoleMaximumLinesChanged )
        self.linesSpinBox.setValue( self.consoleMaximumLines )
        self.checkBoxEnableConsole.stateChanged.connect( self.onEnableConsole )
        self.checkBoxEnableConsole.setChecked( self.consoleEnable )
        
        #setup editor
        self.textEdit = PulseProgramSourceEdit()
        self.textEdit.setupUi(self.textEdit, extraKeywords1=[], extraKeywords2=scriptFunctions)
        self.textEdit.textEdit.currentLineMarkerNum = 9
        self.textEdit.textEdit.markerDefine(QsciScintilla.Background, self.textEdit.textEdit.currentLineMarkerNum) #This is a marker that highlights the background
        self.textEdit.textEdit.setMarkerBackgroundColor(QtGui.QColor(0xd0, 0xff, 0xd0), self.textEdit.textEdit.currentLineMarkerNum)
        self.textEdit.setPlainText(self.script.code)
        self.splitterVertical.insertWidget(0, self.textEdit)
        
        #setup documentation list
        self.getDocs()
        self.docTreeWidget.setHeaderLabels(['Available Script Functions'])
        for funcDef, funcDesc in list(self.docDict.items()):
            itemDef  = QtWidgets.QTreeWidgetItem(self.docTreeWidget, [funcDef])
            self.docTreeWidget.addTopLevelItem(itemDef)
            QtWidgets.QTreeWidgetItem(itemDef, [funcDesc])
            self.docTreeWidget.setWordWrap(True)

        #load recent files, also checks if data was saved correctly and if files still exist
        savedfiles = self.config.get( self.configname+'.recentFiles', OrderedList())
        self.initRecentFiles(savedfiles)
        self.initComboBox()

        #load last opened file
        self.script.fullname = self.config.get( self.configname+'.script.fullname', '' )
        self.initLoad()

        #connect buttons
        self.script.repeat = self.config.get(self.configname+'.repeat',False)
        self.repeatButton.setChecked(self.script.repeat)
        self.repeatButton.clicked.connect( self.onRepeat )
        self.script.slow = self.config.get(self.configname+'.slow',False)
        self.slowButton.setChecked(self.script.slow)
        self.slowButton.clicked.connect( self.onSlow )
        self.revert = self.config.get(self.configname+'.revert',False)
        self.revertButton.setChecked(self.revert)
        self.revertButton.clicked.connect( self.onRevert )
        #File control actions
        self.actionOpen.triggered.connect( self.onLoad )
        self.actionSave.triggered.connect( self.onSave )
        self.actionReset.triggered.connect(self.onReset)
        self.actionNew.triggered.connect( self.onNew )
        #Script control actions
        self.actionStartScript.triggered.connect( self.onStartScript )
        self.actionPauseScript.triggered.connect( self.onPauseScript )
        self.actionStopScript.triggered.connect( self.onStopScript )
        self.actionPauseScriptAndScan.triggered.connect( self.onPauseScriptAndScan )
        self.actionStopScriptAndScan.triggered.connect( self.onStopScriptAndScan )
        #Script finished signal
        self.script.finished.connect( self.onFinished )

        self.setWindowTitle(self.configname)
        self.setWindowIcon(QtGui.QIcon(":/other/icons/Terminal-icon.png"))
        self.statusLabel.setText("Idle")

    def onOpenOptions(self):
        self.optionsWindow.show()
        self.optionsWindow.setWindowState(QtCore.Qt.WindowActive)
        self.optionsWindow.raise_()

    def updateOptions(self):
        self.filenameComboBox.setMaxCount(self.optionsWindow.lineno)
        self.displayFullPathNames = self.optionsWindow.displayPath
        self.script.dispfull = self.optionsWindow.displayPath
        self.defaultExpandAll = self.optionsWindow.defaultExpand
        self.updateFileComboBoxNames(self.displayFullPathNames)

    @QtCore.pyqtSlot()
    def onStartScript(self):
        """Start script button is clicked"""
        if not self.script.isRunning():
            logger = logging.getLogger(__name__)
            message = "script {0} started at {1}".format(self.script.fullname, str(datetime.now()))
            logger.info(message)
            self.writeToConsole(message, color='blue')
            self.onSave()
            self.enableScriptChange(False)
            self.actionPauseScript.setChecked(False)
            self.statusLabel.setText("Script running")
            if self.revert:
                self.savedState = True
                self.saveSettingsState()
            else:
                self.savedState = False
            self.scriptHandler.onStartScript()
            
    @QtCore.pyqtSlot(bool)
    def onPauseScript(self, paused):
        """Pause script button is clicked"""
        logger = logging.getLogger(__name__)
        message = "Script is paused" if paused else "Script is unpaused"
        markerColor = QtGui.QColor("#c0c0ff") if paused else QtGui.QColor(0xd0, 0xff, 0xd0)
        self.textEdit.textEdit.setMarkerBackgroundColor(markerColor, self.textEdit.textEdit.currentLineMarkerNum)
        logger.info(message)
        self.writeToConsole(message, color='blue')
        self.actionPauseScript.setChecked(paused)
        self.scriptHandler.onPauseScript(paused)
        
    @QtCore.pyqtSlot()
    def onStopScript(self):
        """Stop script button is clicked"""
        self.actionPauseScript.setChecked(False)
        self.repeatButton.setChecked(False)
        self.scriptHandler.onStopScript()

    @QtCore.pyqtSlot()
    def onPauseScriptAndScan(self):
        """Pause script and scan button is clicked"""
        logger = logging.getLogger(__name__)
        message = "Script is paused"
        markerColor = QtGui.QColor("#c0c0ff")
        self.textEdit.textEdit.setMarkerBackgroundColor(markerColor, self.textEdit.textEdit.currentLineMarkerNum)
        logger.info(message)
        self.writeToConsole(message, color='blue')
        self.actionPauseScript.setChecked(True)
        self.scriptHandler.onPauseScriptAndScan()
        
    @QtCore.pyqtSlot()
    def onStopScriptAndScan(self):
        """Stop script and scan button is clicked"""
        self.actionPauseScript.setChecked(False)
        self.repeatButton.setChecked(False)
        self.scriptHandler.onStopScriptAndScan()
        
    @QtCore.pyqtSlot()
    def onFinished(self):
        """Runs when script thread finishes. re-enables script GUI."""
        logger = logging.getLogger(__name__)
        self.statusLabel.setText("Idle")
        message = "script {0} finished at {1}".format(self.script.fullname, str(datetime.now()))
        logger.info(message)
        self.writeToConsole(message, color='blue')
        self.textEdit.textEdit.markerDeleteAll()
        self.enableScriptChange(True)
        if self.revert and self.savedState:
            self.restoreSettingsState()
            
    @QtCore.pyqtSlot()
    def onRepeat(self):
        """Repeat button is clicked."""
        logger = logging.getLogger(__name__)
        repeat = self.repeatButton.isChecked()
        message = "Repeat is on" if repeat else "Repeat is off"
        logger.debug(message)
        self.writeToConsole(message)
        self.scriptHandler.onRepeat(repeat)

    @QtCore.pyqtSlot()
    def onRevert(self):
        """Revert button is clicked."""
        self.revert = self.revertButton.isChecked()
        logging.getLogger(__name__).debug("Revert is on" if self.revert else "Revert is off")

    @QtCore.pyqtSlot()
    def onSlow(self):
        """Slow button is clicked."""
        logger = logging.getLogger(__name__)
        slow = self.slowButton.isChecked()
        message = "Slow is on" if slow else "Slow is off"
        logger.debug(message)
        self.writeToConsole(message)
        self.scriptHandler.onSlow(slow)

    @QtCore.pyqtSlot()
    def onNew(self):
        """New button is clicked. Pop up dialog asking for new name, and create file."""
        logger = logging.getLogger(__name__)
        shortname, ok = QtWidgets.QInputDialog.getText(self, 'New script name', 'Enter new file name (optional path specified by localpath/filename): ')
        if ok:
            shortname = str(shortname)
            shortname = shortname.replace(' ', '_') #Replace spaces with underscores
            shortname = shortname.split('.')[0] + '.py'#Take only what's before the '.'
            fullname = self.defaultDir.joinpath(shortname)
            ensurePath(fullname.parent)
            if not fullname.exists():
                try:
                    with fullname.open('w') as f:
                        newFileText = '#' + shortname + ' created ' + str(datetime.now()) + '\n\n'
                        f.write(newFileText)
                except Exception as e:
                    message = "Unable to create new file {0}: {1}".format(shortname, e)
                    logger.error(message)
                    return
            self.loadFile(fullname)
            self.populateTree(fullname)

    def enableScriptChange(self, enabled):
        """Enable or disable any changes to script editor"""
        color = QtGui.QColor("#ffe4e4") if enabled else QtGui.QColor('white')
        self.textEdit.textEdit.setCaretLineVisible(enabled)
        self.textEdit.textEdit.setCaretLineBackgroundColor(color)
        self.textEdit.setReadOnly(not enabled)
        self.filenameComboBox.setDisabled(not enabled)
        self.removeCurrent.setDisabled(not enabled)
        self.actionOpen.setEnabled(enabled)
        self.actionSave.setEnabled(enabled)
        self.actionReset.setEnabled(enabled)
        self.actionNew.setEnabled(enabled)
        self.actionStartScript.setEnabled(enabled)
        self.actionPauseScript.setEnabled(not enabled)
        self.actionStopScript.setEnabled(not enabled)
        self.actionPauseScriptAndScan.setEnabled(not enabled)
        self.actionStopScriptAndScan.setEnabled(not enabled)
        self.allowFileViewerLoad = enabled

    def onComboIndexChange(self, ind):
        """A name is typed into the filename combo box."""
        if ind == 0:
            return False
        if self.script.code != str(self.textEdit.toPlainText()):
            if not self.confirmLoad():
                self.filenameComboBox.setCurrentIndex(0)
                return False
        self.loadFile(self.filenameComboBox.itemData(ind))

    def onLoad(self):
        """The load button is clicked. Open file prompt for file."""
        fullname, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Open Script', self.defaultDir, 'Python scripts (*.py *.pyw)')
        if fullname!="":
            self.loadFile(fullname)

    def loadFile(self, fullname):
        """Load in a file."""
        logger = logging.getLogger(__name__)
        if fullname:
            self.script.fullname = fullname
            with fullname.open("r") as f:
                self.script.code = f.read()
            self.textEdit.setPlainText(self.script.code)
            if self.script.fullname not in self.recentFiles:
                self.filenameComboBox.addItem(self.script.shortname)
            self.recentFiles.add(fullname)
            with BlockSignals(self.filenameComboBox) as w:
                ind = w.findText(str(self.script.shortname)) #having issues with findData Path object comparison
                w.removeItem(ind) #these two lines just push the loaded filename to the top of the combobox
                w.insertItem(0, str(self.script.shortname))
                w.setItemData(0, self.script.fullname)
                w.setCurrentIndex(0)
            logger.info('{0} loaded'.format(self.script.fullname))
            self.initcode = copy.copy(self.script.code)

    def onReset(self):
        """Reset action. Reset file state saved on disk."""
        if self.script.fullname:
            self.loadFile(self.script.fullname)

    def onRemoveCurrent(self):
        """Remove current button is clicked. Remove file from combo box."""
        path = self.filenameComboBox.currentData()
        if path in self.recentFiles:
            self.recentFiles.remove(path)
        self.filenameComboBox.removeItem(0)
        self.loadFile(self.filenameComboBox.currentData())

    def onSave(self):
        """Save action. Save file to disk, and clear any highlighted errors."""
        logger = logging.getLogger(__name__)
        self.script.code = str(self.textEdit.toPlainText())
        self.textEdit.clearHighlightError()
        if self.script.code and self.script.fullname:
            with self.script.fullname.open('w') as f:
                f.write(self.script.code)
                logger.info('{0} saved'.format(self.script.fullname))
    
    def saveConfig(self):
        """Save configuration."""
        self.config[self.configname+'.recentFiles'] = self.recentFiles
        self.config[self.configname+'.script.fullname'] = self.script.fullname
        self.config[self.configname+'.revert'] = self.revert
        self.config[self.configname+'.slow'] = self.script.slow
        self.config[self.configname+'.repeat'] = self.script.repeat
        self.config[self.configname+'.isVisible'] = self.isVisible()
        self.config[self.configname+'.ScriptingUi.pos'] = self.pos()
        self.config[self.configname+'.ScriptingUi.size'] = self.size()
        self.config[self.configname+".splitterHorizontal"] = self.splitterHorizontal.saveState()
        self.config[self.configname+".splitterVertical"] = self.splitterVertical.saveState()
        self.config[self.configname+'.consoleMaximumLinesNew'] = self.consoleMaximumLines
        self.config[self.configname+'.consoleEnable'] = self.consoleEnable
       
    def show(self):
        pos = self.config.get(self.configname+'.ScriptingUi.pos')
        size = self.config.get(self.configname+'.ScriptingUi.size')
        splitterHorizontalState = self.config.get(self.configname+".splitterHorizontal")
        splitterVerticalState = self.config.get(self.configname+".splitterVertical")
        if pos:
            self.move(pos)
        if size:
            self.resize(size)
        if splitterHorizontalState:
            self.splitterHorizontal.restoreState(splitterHorizontalState)
        if splitterVerticalState:
            self.splitterVertical.restoreState(splitterVerticalState)
        QtWidgets.QDialog.show(self)

    def onClose(self):
        self.saveConfig()
        self.hide()
        
    def onClearConsole(self):
        self.textEditConsole.clear()

    def onConsoleMaximumLinesChanged(self, maxlines):
        self.consoleMaximumLines = maxlines
        self.textEditConsole.document().setMaximumBlockCount(maxlines)

    def onEnableConsole(self, state):
        self.consoleEnable = state==QtCore.Qt.Checked

    def markLocation(self, lines):
        """mark a specified location""" 
        if lines:
            self.textEdit.textEdit.markerDeleteAll()
            for line in lines:
                self.textEdit.textEdit.markerAdd(line-1, self.textEdit.textEdit.ARROW_MARKER_NUM)
                self.textEdit.textEdit.markerAdd(line-1, self.textEdit.textEdit.currentLineMarkerNum)

    def markError(self, lines, message):
        """mark error at specified lines, and show message"""
        if lines != []:
            for line in lines:
                self.textEdit.highlightError(message, line)

    def writeToConsole(self, message, error=False, color=''):
        if self.consoleEnable:
            message = str(message)
            cursor = self.textEditConsole.textCursor()
            cursor.movePosition(QtGui.QTextCursor.End)
            textColor = ('red' if error else 'black') if color=='' else color
            self.textEditConsole.setUpdatesEnabled(False)
            if textColor == 'black':
                self.textEditConsole.insertPlainText(message+'\n')
            else:
                self.textEditConsole.insertHtml(str('<p><font color='+textColor+'>'+message+'</font><br></p>'))
            self.textEditConsole.setUpdatesEnabled(True)
            self.textEditConsole.setTextCursor(cursor)
            self.textEditConsole.ensureCursorVisible()

    def getDocs(self):
        """Assemble the script function documentation into a dictionary"""
        self.docDict = OrderedDict()
        for doc in scriptDocs:
            docsplit = doc.splitlines() 
            defLine = docsplit.pop(0)
            docsplit = [line.strip() for line in docsplit]
            docsplit = '\n'.join(docsplit)
            self.docDict[defLine] = docsplit

    def updateValidator(self):
        """Make the validator match the recentFiles list. Uses regExp \\b(f1|f2|f3...)\\b, where fn are filenames."""
        regExp = '\\b('
        for shortname in self.recentFiles:
            if shortname:
                regExp += shortname + '|'
        regExp = regExp[:-1] #drop last pipe symbol
        regExp += ')\\b'
        self.filenameComboBox.validator().setRegExp(QtCore.QRegExp(regExp))

    def saveSettingsState(self):
        """Save the state of the scan, evaluation, and analysis"""
        self.originalState = dict()
        self.originalState['scan'] = self.experimentUi.tabDict['Scan'].scanControlWidget.settingsName
        self.originalState['evaluation'] = self.experimentUi.tabDict['Scan'].evaluationControlWidget.settingsName
        self.originalState['analysis'] = self.experimentUi.tabDict['Scan'].analysisControlWidget.currentAnalysisName

    def restoreSettingsState(self):
        """Restore the settings to their original values"""
        for name, value in self.scriptHandler.globalVariablesRevertDict.items():
            self.experimentUi.globalVariablesUi.model.update([('Global', name, value)])
        self.experimentUi.tabDict['Scan'].scanControlWidget.loadSetting(self.originalState['scan'])
        self.experimentUi.tabDict['Scan'].evaluationControlWidget.loadSetting(self.originalState['evaluation'])
        self.experimentUi.tabDict['Scan'].analysisControlWidget.onLoadAnalysisConfiguration(self.originalState['analysis'])