Ejemplo n.º 1
0
class EditorPanel(Panel):
    """Provides a text editor specialized for entering scripts.

    Together with actions such as `Run` or `Simulate` it gives the user the
    opportunity to create and check measurement scripts.  The editor widget
    uses `QScintilla` if it is installed, and a standard text edit box
    otherwise.

    Options:

    * ``tools`` (default None) -- a list of `tools` which may configure some
      special commands or scripts.  The tools can generate code to insert
      into the editor window.  The access to these tools will be given via a
      special menu ``Editor tools``.
    * ``show_browser`` (default True) -- Toggle the default visibility of
      the Script Browser widget.
    * ``sim_window`` -- how to display dry run results: either ``"inline"``
      (in a dock widget in the panel, the default), ``"single"`` (in an
      external window, the same for each run), or ``"multi"`` (each run opens
      a new window).
    """

    panelName = 'User editor'

    def __init__(self, parent, client, options):
        Panel.__init__(self, parent, client, options)
        loadUi(self, 'panels/editor.ui')

        self.window = parent
        self.custom_font = None
        self.custom_back = None

        self.mainwindow.codeGenerated.connect(self.on_codeGenerated)

        if not has_scintilla:
            self.actionComment.setEnabled(False)

        self.menus = None
        self.bar = None
        self.current_status = None
        self.recentf_actions = []
        self.searchdlg = None
        self.menuRecent = QMenu('Recent files')

        self.menuToolsActions = []

        for fn in self.recentf:
            action = QAction(fn.replace('&', '&&'), self)
            action.setData(fn)
            action.triggered.connect(self.openRecentFile)
            self.recentf_actions.append(action)
            self.menuRecent.addAction(action)

        self.tabber = QTabWidget(self, tabsClosable=True, documentMode=True)
        self.tabber.currentChanged.connect(self.on_tabber_currentChanged)
        self.tabber.tabCloseRequested.connect(self.on_tabber_tabCloseRequested)

        self.toolconfig = options.get('tools')
        self.sim_window = options.get('sim_window', 'inline')
        if self.sim_window not in ('single', 'multi', 'inline'):
            self.log.warning('invalid sim_window option %r, using inline',
                             self.sim_window)
            self.sim_window = 'inline'

        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        hlayout.addWidget(self.tabber)
        self.mainFrame.setLayout(hlayout)

        self.editors = []    # tab index -> editor
        self.filenames = {}  # editor -> filename
        self.watchers = {}   # editor -> QFileSystemWatcher
        self.currentEditor = None

        self.saving = False  # True while saving
        self.warnWidget.hide()

        self.simFrame = SimResultFrame(self, None, self.client)
        self.simPaneFrame.layout().addWidget(self.simFrame)
        self.simPane.hide()
        self.simWindows = []

        self.splitter.restoreState(self.splitterstate)
        self.treeModel = QFileSystemModel()
        idx = self.treeModel.setRootPath('/')
        self.treeModel.setNameFilters(['*.py', '*.txt'])
        self.treeModel.setNameFilterDisables(False)  # hide them
        self.fileTree.setModel(self.treeModel)
        self.fileTree.header().hideSection(1)
        self.fileTree.header().hideSection(2)
        self.fileTree.header().hideSection(3)
        self.fileTree.header().hide()
        self.fileTree.setRootIndex(idx)
        if not options.get('show_browser', True):
            self.scriptsPane.hide()
        self.actionShowScripts = self.scriptsPane.toggleViewAction()
        self.actionShowScripts.setText('Show Script Browser')

        self.activeGroup = QActionGroup(self)
        self.activeGroup.addAction(self.actionRun)
        self.activeGroup.addAction(self.actionSimulate)
        self.activeGroup.addAction(self.actionUpdate)

        client.simresult.connect(self.on_client_simresult)
        if self.client.connected:
            self.on_client_connected()
        else:
            self.on_client_disconnected()
        client.connected.connect(self.on_client_connected)
        client.disconnected.connect(self.on_client_disconnected)
        client.setup.connect(self.on_client_connected)
        client.cache.connect(self.on_client_cache)
        client.experiment.connect(self.on_client_experiment)

        if self.openfiles:
            for fn in self.openfiles:
                self.openFile(fn, quiet=True)
        else:
            self.newFile()

    def __del__(self):
        # On some systems the  QFilesystemWatchers deadlock on application exit
        # so destroy them explicitly
        self.watchers.clear()

    def setViewOnly(self, viewonly):
        self.activeGroup.setEnabled(not viewonly)

    def getMenus(self):
        menuFile = QMenu('&File', self)
        menuFile.addAction(self.actionNew)
        menuFile.addAction(self.actionOpen)
        menuFile.addAction(self.menuRecent.menuAction())
        menuFile.addAction(self.actionSave)
        menuFile.addAction(self.actionSaveAs)
        menuFile.addAction(self.actionReload)
        menuFile.addSeparator()
        menuFile.addAction(self.actionPrint)

        menuView = QMenu('&View', self)
        menuView.addAction(self.actionShowScripts)

        menuEdit = QMenu('&Edit', self)
        menuEdit.addAction(self.actionUndo)
        menuEdit.addAction(self.actionRedo)
        menuEdit.addSeparator()
        menuEdit.addAction(self.actionCut)
        menuEdit.addAction(self.actionCopy)
        menuEdit.addAction(self.actionPaste)
        menuEdit.addSeparator()
        menuEdit.addAction(self.actionComment)
        menuEdit.addSeparator()
        menuEdit.addAction(self.actionFind)

        menuScript = QMenu('&Script', self)
        menuScript.addSeparator()
        menuScript.addAction(self.actionRun)
        menuScript.addAction(self.actionSimulate)
        menuScript.addAction(self.actionUpdate)
        menuScript.addSeparator()
        menuScript.addAction(self.actionGet)

        if self.toolconfig:
            menuTools = QMenu('Editor t&ools', self)
            createToolMenu(self, self.toolconfig, menuTools)
            menus = [menuFile, menuView, menuEdit, menuScript, menuTools]
        else:
            menus = [menuFile, menuView, menuEdit, menuScript]

        self.menus = menus
        return self.menus

    def getToolbars(self):
        if not self.bar:
            bar = QToolBar('Editor')
            bar.addAction(self.actionNew)
            bar.addAction(self.actionOpen)
            bar.addAction(self.actionSave)
            bar.addSeparator()
            bar.addAction(self.actionPrint)
            bar.addSeparator()
            bar.addAction(self.actionUndo)
            bar.addAction(self.actionRedo)
            bar.addSeparator()
            bar.addAction(self.actionCut)
            bar.addAction(self.actionCopy)
            bar.addAction(self.actionPaste)
            bar.addSeparator()
            bar.addAction(self.actionRun)
            bar.addAction(self.actionSimulate)
            bar.addAction(self.actionGet)
            bar.addAction(self.actionUpdate)
            showToolText(bar, self.actionRun)
            showToolText(bar, self.actionSimulate)
            showToolText(bar, self.actionGet)
            showToolText(bar, self.actionUpdate)
            self.bar = bar

        return [self.bar]

    def updateStatus(self, status, exception=False):
        self.current_status = status

    def setCustomStyle(self, font, back):
        self.custom_font = font
        self.custom_back = back
        self.simFrame.simOutView.setFont(font)
        self.simFrame.simOutViewErrors.setFont(font)
        for editor in self.editors:
            self._updateStyle(editor)

    def _updateStyle(self, editor):
        if self.custom_font is None:
            return
        bold = QFont(self.custom_font)
        bold.setBold(True)
        if has_scintilla:
            lexer = editor.lexer()
            lexer.setDefaultFont(self.custom_font)
            for i in range(16):
                lexer.setFont(self.custom_font, i)
            # make keywords bold
            lexer.setFont(bold, 5)
        else:
            editor.setFont(self.custom_font)
        if has_scintilla:
            lexer.setPaper(self.custom_back)
        else:
            setBackgroundColor(editor, self.custom_back)

    def enableFileActions(self, on):
        for action in [
            self.actionSave, self.actionSaveAs, self.actionReload,
            self.actionPrint, self.actionUndo, self.actionRedo, self.actionCut,
            self.actionCopy, self.actionPaste, self.actionFind,
        ]:
            action.setEnabled(on)
        self.enableExecuteActions(self.client.isconnected)
        for action in [self.actionComment]:
            action.setEnabled(on and has_scintilla)

    def enableExecuteActions(self, on):
        for action in [
            self.actionRun, self.actionSimulate, self.actionGet,
            self.actionUpdate
        ]:
            action.setEnabled(self.client.isconnected)

    def on_codeGenerated(self, code):
        if self.currentEditor:
            self.currentEditor.beginUndoAction()
            if self.currentEditor.text():
                res = OverwriteQuestion().exec_()
                if res == QMessageBox.Apply:
                    self.currentEditor.clear()
                elif res == QMessageBox.Cancel:
                    return
            # append() and setText() would clear undo history in QScintilla,
            # therefore we use these calls
            self.currentEditor.moveToEnd()
            self.currentEditor.insert(code)
            self.currentEditor.endUndoAction()
        else:
            self.showError('No script is opened at the moment.')

    def on_tabber_currentChanged(self, index):
        self.enableFileActions(index >= 0)
        if index == -1:
            self.currentEditor = None
            self.window.setWindowTitle('%s editor' % self.mainwindow.instrument)
            return
        editor = self.editors[index]
        fn = self.filenames[editor]
        if fn:
            self.window.setWindowTitle('%s[*] - %s editor' %
                                       (fn, self.mainwindow.instrument))
        else:
            self.window.setWindowTitle('New[*] - %s editor' %
                                       self.mainwindow.instrument)
        self.window.setWindowModified(editor.isModified())
        self.actionSave.setEnabled(editor.isModified())
        self.actionUndo.setEnabled(editor.isModified())
        self.currentEditor = editor
        if self.searchdlg:
            self.searchdlg.setEditor(editor)

    def on_tabber_tabCloseRequested(self, index):
        editor = self.editors[index]
        self._close(editor)

    def _close(self, editor):
        if not self.checkDirty(editor):
            return
        index = self.editors.index(editor)
        del self.editors[index]
        del self.filenames[editor]
        del self.watchers[editor]
        self.tabber.removeTab(index)

    def setDirty(self, editor, dirty):
        if editor is self.currentEditor:
            self.actionSave.setEnabled(dirty)
            self.actionUndo.setEnabled(dirty)
            self.window.setWindowModified(dirty)
            index = self.tabber.currentIndex()
            tt = self.tabber.tabText(index).rstrip('*')
            self.tabber.setTabText(index, tt + (dirty and '*' or ''))

    def loadSettings(self, settings):
        self.recentf = settings.value('recentf') or []
        self.splitterstate = settings.value('splitter', '', QByteArray)
        self.openfiles = settings.value('openfiles') or []

    def saveSettings(self, settings):
        settings.setValue('splitter', self.splitter.saveState())
        settings.setValue('openfiles',
                          [self.filenames[e] for e in self.editors
                           if self.filenames[e]])

    def requestClose(self):
        for editor in self.editors:
            if not self.checkDirty(editor):
                return False
        return True

    def createEditor(self):
        if has_scintilla:
            editor = QsciScintillaCustom(self)
            lexer = QsciLexerPython(editor)
            editor.setUtf8(True)
            editor.setLexer(lexer)
            editor.setAutoIndent(True)
            editor.setEolMode(QsciScintilla.EolUnix)
            editor.setIndentationsUseTabs(False)
            editor.setIndentationGuides(True)
            editor.setTabIndents(True)
            editor.setBackspaceUnindents(True)
            editor.setTabWidth(4)
            editor.setIndentationWidth(0)
            editor.setBraceMatching(QsciScintilla.SloppyBraceMatch)
            editor.setFolding(QsciScintilla.PlainFoldStyle)
            editor.setIndentationGuidesForegroundColor(QColor("#CCC"))
            editor.setWrapMode(QsciScintilla.WrapCharacter)
            editor.setMarginLineNumbers(1, True)
            editor.setMarginWidth(
                1, 5 + 4 * QFontMetrics(editor.font()).averageCharWidth())
        else:
            editor = QScintillaCompatible(self)
        # editor.setFrameStyle(0)
        editor.modificationChanged.connect(
            lambda dirty: self.setDirty(editor, dirty))
        self._updateStyle(editor)
        return editor

    def on_client_connected(self):
        self.enableExecuteActions(True)
        self._set_scriptdir()

    def on_client_disconnected(self):
        self.enableExecuteActions(False)

    def _set_scriptdir(self):
        initialdir = self.client.eval('session.experiment.scriptpath', '')
        if initialdir:
            idx = self.treeModel.setRootPath(initialdir)
            self.fileTree.setRootIndex(idx)

    def on_client_cache(self, data):
        (_time, key, _op, _value) = data
        if key.endswith('/scriptpath'):
            self.on_client_connected()

    def on_client_simresult(self, data):
        if self.sim_window == 'inline':
            self.actionSimulate.setEnabled(True)

    def on_client_experiment(self, data):
        (_, proptype) = data
        self._set_scriptdir()
        self.simPane.hide()
        if proptype == 'user':
            # close existing tabs when switching TO a user experiment
            for index in range(len(self.editors) - 1, -1, -1):
                self.on_tabber_tabCloseRequested(index)
            # if all tabs have been closed, open a new file
            if not self.tabber.count():
                self.on_actionNew_triggered()

    def on_fileTree_doubleClicked(self, idx):
        fpath = self.treeModel.filePath(idx)
        for i, editor in enumerate(self.editors):
            if self.filenames[editor] == fpath:
                self.tabber.setCurrentIndex(i)
                return
        self.openFile(fpath)

    @pyqtSlot()
    def on_actionPrint_triggered(self):
        if has_scintilla:
            printer = Printer()
            printer.setOutputFileName('')
            printer.setDocName(self.filenames[self.currentEditor])
            # printer.setFullPage(True)
            if QPrintDialog(printer, self).exec_() == QDialog.Accepted:
                lexer = self.currentEditor.lexer()
                bgcolor = lexer.paper(0)
                # printer prints background color too, so set it to white
                lexer.setPaper(Qt.white)
                printer.printRange(self.currentEditor)
                lexer.setPaper(bgcolor)
        else:
            printer = QPrinter()
            printer.setOutputFileName('')
            if QPrintDialog(printer, self).exec_() == QDialog.Accepted:
                getattr(self.currentEditor, 'print')(printer)

    def validateScript(self):
        script = self.currentEditor.text()
        # XXX: this does not apply to .txt (SPM) scripts
        # try:
        #    compile(script, 'script', 'exec')
        # except SyntaxError as err:
        #    self.showError('Syntax error in script: %s' % err)
        #    self.currentEditor.setCursorPosition(err.lineno - 1, err.offset)
        #    return
        return script

    @pyqtSlot()
    def on_actionRun_triggered(self):
        script = self.validateScript()
        if script is None:
            return
        if not self.checkDirty(self.currentEditor, askonly=True):
            return
        if self.current_status != 'idle':
            if not self.askQuestion('A script is currently running, do you '
                                    'want to queue this script?', True):
                return
        self.client.run(script, self.filenames[self.currentEditor])

    @pyqtSlot()
    def on_actionSimulate_triggered(self):
        script = self.validateScript()
        if script is None:
            return
        if not self.checkDirty(self.currentEditor, askonly=True):
            return
        simuuid = str(uuid1())
        filename = self.filenames[self.currentEditor]
        if self.sim_window == 'inline':
            self.actionSimulate.setEnabled(False)
            self.simFrame.simuuid = simuuid
            self.simFrame.clear()
            self.simPane.setWindowTitle('Dry run results - %s' % filename)
            self.simPane.show()
        else:
            if self.sim_window == 'multi' or not self.simWindows:
                window = SimResultFrame(None, self, self.client)
                window.setWindowTitle('Dry run results - %s' % filename)
                window.layout().setContentsMargins(6, 6, 6, 6)
                window.simOutView.setFont(self.simFrame.simOutView.font())
                window.simOutViewErrors.setFont(self.simFrame.simOutView.font())
                window.show()
                self.simWindows.append(window)
            else:
                window = self.simWindows[0]
                window.clear()
                window.setWindowTitle('Dry run results - %s' % filename)
                window.activateWindow()
            window.simuuid = simuuid
        self.client.tell('simulate', filename, script, simuuid)

    @pyqtSlot()
    def on_actionUpdate_triggered(self):
        script = self.validateScript()
        if script is None:
            return
        if not self.checkDirty(self.currentEditor, askonly=True):
            return
        reason, ok = QInputDialog.getText(
            self, 'Update reason', 'For the logbook, you can enter a reason '
            'for the update here:', text='no reason specified')
        if not ok:
            return
        self.client.tell('update', script, reason)

    @pyqtSlot()
    def on_actionGet_triggered(self):
        script = self.client.ask('getscript')
        if script is not None:
            editor = self.newFile()
            editor.setText(script)

    def checkDirty(self, editor, askonly=False):
        if not editor.isModified():
            return True
        if self.filenames[editor]:
            message = 'Save changes in %s before continuing?' % \
                self.filenames[editor]
        else:
            message = 'Save new file before continuing?'
        buttons = QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
        if askonly:
            buttons = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
        rc = QMessageBox.question(self, 'User Editor', message, buttons)
        if rc in (QMessageBox.Save, QMessageBox.Yes):
            return self.saveFile(editor)
        if rc in (QMessageBox.Discard, QMessageBox.No):
            return True
        return False

    def on_fileSystemWatcher_fileChanged(self, filename):
        if self.saving:
            return
        editor = watcher = None
        for editor, watcher in self.watchers.items():
            if watcher is self.sender():
                break
        else:
            return
        if editor.isModified():
            # warn the user
            self.warnText.setText(
                'The file %r has changed on disk, but has also been edited'
                ' here.\nPlease use either File-Reload to load the'
                ' version on disk or File-Save to save this version.'
                % self.filenames[editor])
            self.warnWidget.show()
        else:
            # reload without asking
            try:
                with open(self.filenames[editor]) as f:
                    text = f.read()
            except Exception:
                return
            editor.setText(text)
            editor.setModified(False)
        # re-add the filename to the watcher if it was deleted
        # (happens for programs that do delete-write on save)
        if not watcher.files():
            watcher.addPath(self.filenames[editor])

    @pyqtSlot()
    def on_actionNew_triggered(self):
        self.newFile()

    def newFile(self):
        editor = self.createEditor()
        editor.setModified(False)
        self.editors.append(editor)
        self.filenames[editor] = ''
        self.watchers[editor] = QFileSystemWatcher(self)
        self.watchers[editor].fileChanged.connect(
            self.on_fileSystemWatcher_fileChanged)
        self.tabber.addTab(editor, '(New script)')
        self.tabber.setCurrentWidget(editor)
        self.simFrame.clear()
        editor.setFocus()
        return editor

    @pyqtSlot()
    def on_actionOpen_triggered(self):
        if self.currentEditor is not None and self.filenames[self.currentEditor]:
            initialdir = path.dirname(self.filenames[self.currentEditor])
        else:
            initialdir = self.client.eval('session.experiment.scriptpath', '')
        fn = QFileDialog.getOpenFileName(self, 'Open script', initialdir,
                                         'Script files (*.py *.txt)')[0]
        if not fn:
            return
        self.openFile(fn)
        self.addToRecentf(fn)

    @pyqtSlot()
    def on_actionReload_triggered(self):
        fn = self.filenames[self.currentEditor]
        if not fn:
            return
        if not self.checkDirty(self.currentEditor):
            return
        try:
            with open(fn, 'r') as f:
                text = f.read()
        except Exception as err:
            return self.showError('Opening file failed: %s' % err)
        self.currentEditor.setText(text)
        self.simFrame.clear()

    def openRecentFile(self):
        self.openFile(self.sender().data())

    def openFile(self, fn, quiet=False):
        try:
            with open(fn.encode(sys.getfilesystemencoding())) as f:
                text = f.read()
        except Exception as err:
            if quiet:
                return
            return self.showError('Opening file failed: %s' % err)

        editor = self.createEditor()
        editor.setText(text)
        editor.setModified(False)

        # replace tab if it's a single new file
        if len(self.editors) == 1 and not self.filenames[self.editors[0]] and \
           not self.editors[0].isModified():
            self._close(self.editors[0])

        self.editors.append(editor)
        self.filenames[editor] = fn
        self.watchers[editor] = QFileSystemWatcher(self)
        self.watchers[editor].fileChanged.connect(
            self.on_fileSystemWatcher_fileChanged)
        self.watchers[editor].addPath(fn)
        self.tabber.addTab(editor, path.basename(fn))
        self.tabber.setCurrentWidget(editor)
        self.simFrame.clear()
        editor.setFocus()

    def addToRecentf(self, fn):
        new_action = QAction(fn.replace('&', '&&'), self)
        new_action.setData(fn)
        new_action.triggered.connect(self.openRecentFile)
        if self.recentf_actions:
            self.menuRecent.insertAction(self.recentf_actions[0], new_action)
            self.recentf_actions.insert(0, new_action)
            del self.recentf_actions[10:]
        else:
            self.menuRecent.addAction(new_action)
            self.recentf_actions.append(new_action)
        with self.sgroup as settings:
            settings.setValue('recentf',
                              [a.data() for a in self.recentf_actions])

    @pyqtSlot()
    def on_actionSave_triggered(self):
        self.saveFile(self.currentEditor)
        self.window.setWindowTitle(
            '%s[*] - %s editor' %
            (self.filenames[self.currentEditor], self.mainwindow.instrument))

    @pyqtSlot()
    def on_actionSaveAs_triggered(self):
        self.saveFileAs(self.currentEditor)
        self.window.setWindowTitle(
            '%s[*] - %s editor' %
            (self.filenames[self.currentEditor], self.mainwindow.instrument))

    def saveFile(self, editor):
        if not self.filenames[editor]:
            return self.saveFileAs(editor)

        try:
            self.saving = True
            try:
                with open(self.filenames[editor], 'w') as f:
                    f.write(editor.text())
            finally:
                self.saving = False
        except Exception as err:
            self.showError('Writing file failed: %s' % err)
            return False

        self.watchers[editor].addPath(self.filenames[editor])
        editor.setModified(False)
        return True

    def saveFileAs(self, editor):
        if self.filenames[editor]:
            initialdir = path.dirname(self.filenames[editor])
        else:
            initialdir = self.client.eval('session.experiment.scriptpath', '')
        if self.client.eval('session.spMode', False):
            defaultext = '.txt'
            flt = 'Script files (*.txt *.py)'
        else:
            defaultext = '.py'
            flt = 'Script files (*.py *.txt)'
        fn = QFileDialog.getSaveFileName(self, 'Save script', initialdir, flt)[0]
        if not fn:
            return False
        if not fn.endswith(('.py', '.txt')):
            fn += defaultext
        self.addToRecentf(fn)
        self.watchers[editor].removePath(self.filenames[editor])
        self.filenames[editor] = fn
        self.tabber.setTabText(self.editors.index(editor), path.basename(fn))
        return self.saveFile(editor)

    @pyqtSlot()
    def on_actionFind_triggered(self):
        if not self.searchdlg:
            self.searchdlg = SearchDialog(self, self.currentEditor,
                                          has_scintilla)
        self.searchdlg.setEditor(self.currentEditor)
        self.searchdlg.show()

    @pyqtSlot()
    def on_actionUndo_triggered(self):
        self.currentEditor.undo()

    @pyqtSlot()
    def on_actionRedo_triggered(self):
        self.currentEditor.redo()

    @pyqtSlot()
    def on_actionCut_triggered(self):
        self.currentEditor.cut()

    @pyqtSlot()
    def on_actionCopy_triggered(self):
        self.currentEditor.copy()

    @pyqtSlot()
    def on_actionPaste_triggered(self):
        self.currentEditor.paste()

    @pyqtSlot()
    def on_actionComment_triggered(self):
        clen = len(COMMENT_STR)
        # act on selection?
        if self.currentEditor.hasSelectedText():
            # get the selection boundaries
            line1, index1, line2, index2 = self.currentEditor.getSelection()
            if index2 == 0:
                endLine = line2 - 1
            else:
                endLine = line2
            assert endLine >= line1

            self.currentEditor.beginUndoAction()
            # iterate over the lines
            action = []
            for line in range(line1, endLine + 1):
                if self.currentEditor.text(line).startswith(COMMENT_STR):
                    self.currentEditor.setSelection(line, 0, line, clen)
                    self.currentEditor.removeSelectedText()
                    action.append(-1)
                else:
                    self.currentEditor.insertAt(COMMENT_STR, line, 0)
                    action.append(1)
            # adapt original selection boundaries
            if index1 > 0:
                if action[0] == 1:
                    index1 += clen
                else:
                    index1 = max(0, index1 - clen)
            if endLine > line1 and index2 > 0:
                if action[-1] == 1:
                    index2 += clen
                else:
                    index2 = max(0, index2 - clen)
            # restore selection accordingly
            self.currentEditor.setSelection(line1, index1, line2, index2)
            self.currentEditor.endUndoAction()
        else:
            # comment line
            line, _ = self.currentEditor.getCursorPosition()
            self.currentEditor.beginUndoAction()
            if self.currentEditor.text(line).startswith(COMMENT_STR):
                self.currentEditor.setSelection(line, 0, line, clen)
                self.currentEditor.removeSelectedText()
            else:
                self.currentEditor.insertAt(COMMENT_STR, line, 0)
            self.currentEditor.endUndoAction()
Ejemplo n.º 2
0
class ELogPanel(Panel):
    """Provides a HTML widget for the electronic logbook."""

    panelName = 'Electronic logbook'

    def __init__(self, parent, client, options):
        Panel.__init__(self, parent, client, options)
        loadUi(self, 'panels/elog.ui')
        self.preview = QWebView(self)
        self.frame.layout().addWidget(self.preview)

        self.timer = QTimer(self,
                            singleShot=True,
                            timeout=self.on_timer_timeout)
        self.propdir = None

        self.menus = None
        self.bar = None

        if client.isconnected:
            self.on_client_connected()
        client.connected.connect(self.on_client_connected)
        client.setup.connect(self.on_client_connected)
        client.experiment.connect(self.on_client_experiment)

        self.activeGroup = QActionGroup(self)
        self.activeGroup.addAction(self.actionAddComment)
        self.activeGroup.addAction(self.actionAddRemark)
        self.activeGroup.addAction(self.actionAttachFile)
        self.activeGroup.addAction(self.actionNewSample)

        page = self.preview.page()
        if hasattr(page, 'setForwardUnsupportedContent'):  # QWebKit only
            page.setForwardUnsupportedContent(True)
            page.unsupportedContent.connect(self.on_page_unsupportedContent)

    def getMenus(self):
        if not self.menus:
            menu1 = QMenu('&Browser', self)
            menu1.addAction(self.actionBack)
            menu1.addAction(self.actionForward)
            menu1.addSeparator()
            menu1.addAction(self.actionRefresh)
            menu1.addAction(self.actionPrint)
            menu2 = QMenu('&Logbook', self)
            menu2.addAction(self.actionAddComment)
            menu2.addAction(self.actionAddRemark)
            menu2.addSeparator()
            menu2.addAction(self.actionAttachFile)
            menu2.addSeparator()
            menu2.addAction(self.actionNewSample)
            self.menus = [menu1, menu2]

        return self.menus

    def getToolbars(self):
        if not self.bar:
            bar = QToolBar('Logbook')
            bar.addAction(self.actionBack)
            bar.addAction(self.actionForward)
            bar.addSeparator()
            bar.addAction(self.actionRefresh)
            bar.addAction(self.actionPrint)
            bar.addSeparator()
            bar.addAction(self.actionAddComment)
            bar.addAction(self.actionAddRemark)
            bar.addSeparator()
            bar.addAction(self.actionNewSample)
            bar.addAction(self.actionAttachFile)
            bar.addSeparator()
            box = QLineEdit(self)
            btn = QPushButton('Search', self)
            bar.addWidget(box)
            bar.addWidget(btn)

            def callback():
                if hasattr(QWebPage, 'FindWrapsAroundDocument'):  # WebKit
                    self.preview.findText(box.text(),
                                          QWebPage.FindWrapsAroundDocument)
                else:
                    # WebEngine wraps automatically
                    self.preview.findText(box.text())

            box.returnPressed.connect(callback)
            btn.clicked.connect(callback)
            self.bar = bar

        return [self.bar]

    def setViewOnly(self, viewonly):
        self.activeGroup.setEnabled(not viewonly)

    def on_timer_timeout(self):
        if hasattr(self.preview.page(), 'mainFrame'):  # QWebKit only
            try:
                frame = self.preview.page().mainFrame().childFrames()[1]
            except IndexError:
                self.log.error('No logbook seems to be loaded.')
                self.on_client_connected()
                return
            scrollval = frame.scrollBarValue(Qt.Vertical)
            was_at_bottom = scrollval == frame.scrollBarMaximum(Qt.Vertical)

            # restore current scrolling position in document on reload
            def callback(new_size):
                nframe = self.preview.page().mainFrame().childFrames()[1]
                if was_at_bottom:
                    nframe.setScrollBarValue(
                        Qt.Vertical, nframe.scrollBarMaximum(Qt.Vertical))
                else:
                    nframe.setScrollBarValue(Qt.Vertical, scrollval)
                self.preview.loadFinished.disconnect(callback)

            self.preview.loadFinished.connect(callback)
        self.preview.reload()

    def on_client_connected(self):
        self._update_content()

    def on_client_experiment(self, data):
        self._update_content()

    def _update_content(self):
        self.propdir = self.client.eval('session.experiment.proposalpath', '')
        if not self.propdir:
            return
        logfile = path.abspath(
            path.join(self.propdir, 'logbook', 'logbook.html'))
        if path.isfile(logfile):
            self.preview.load(QUrl('file://' + logfile))
        else:
            self.preview.setHtml(
                '<style>body { font-family: sans-serif; }</style>'
                '<p><b>The logbook HTML file does not seem to exist.</b></p>'
                '<p>Please check that the file is created and accessible on '
                '<b>your local computer</b> at %s.  Then click '
                '"refresh" above.' % html.escape(path.normpath(logfile)))

    def on_page_unsupportedContent(self, reply):
        if reply.url().scheme() != 'file':
            return
        filename = reply.url().path()
        if filename.endswith('.dat'):
            content = open(filename, encoding='utf-8', errors='replace').read()
            window = QMainWindow(self)
            window.resize(600, 800)
            window.setWindowTitle(filename)
            widget = QTextEdit(window)
            widget.setFontFamily('monospace')
            window.setCentralWidget(widget)
            widget.setText(content)
            window.show()
        else:
            # try to open the link with host computer default application
            try:
                QDesktopServices.openUrl(reply.url())
            except Exception:
                pass

    def on_refreshLabel_linkActivated(self, link):
        if link == 'refresh':
            self.on_timer_timeout()
        elif link == 'back':
            self.preview.back()
        elif link == 'forward':
            self.preview.forward()

    @pyqtSlot()
    def on_actionRefresh_triggered(self):
        # if for some reason, we have the wrong proposal path, update here
        propdir = self.client.eval('session.experiment.proposalpath', '')
        if propdir and propdir != self.propdir:
            self._update_content()
        else:
            self.on_timer_timeout()

    @pyqtSlot()
    def on_actionBack_triggered(self):
        self.preview.back()

    @pyqtSlot()
    def on_actionForward_triggered(self):
        self.preview.forward()

    @pyqtSlot()
    def on_actionNewSample_triggered(self):
        name, ok = QInputDialog.getText(self, 'New sample',
                                        'Please enter the new sample name:')
        if not ok or not name:
            return
        self.client.eval('NewSample(%r)' % name)
        self.timer.start(750)

    @pyqtSlot()
    def on_actionAddRemark_triggered(self):
        remark, ok = QInputDialog.getText(
            self, 'New remark',
            'Please enter the remark.  The remark will be added to the logbook '
            'as a heading and will also appear in the data files.')
        if not ok or not remark:
            return
        self.client.eval('Remark(%r)' % remark)
        self.timer.start(750)

    @pyqtSlot()
    def on_actionAddComment_triggered(self):
        dlg = dialogFromUi(self, 'panels/elog_comment.ui')
        dlg.helpFrame.setVisible(False)
        dlg.mdLabel.linkActivated.connect(
            lambda link: dlg.helpFrame.setVisible(True))
        if dlg.exec_() != QDialog.Accepted:
            return
        text = dlg.freeFormText.toPlainText()
        if not text:
            return
        self.client.eval('LogEntry(%r)' % text)
        self.timer.start(750)

    @pyqtSlot()
    def on_actionAttachFile_triggered(self):
        dlg = dialogFromUi(self, 'panels/elog_attach.ui')

        def on_fileSelect_clicked():
            self.selectInputFile(dlg.fileName, 'Choose a file to attach')
            dlg.fileRename.setFocus()

        dlg.fileSelect.clicked.connect(on_fileSelect_clicked)
        if dlg.exec_() != QDialog.Accepted:
            return
        fname = dlg.fileName.text()
        if not path.isfile(fname):
            return self.showError('The given file name is not a valid file.')
        newname = dlg.fileRename.text()
        if not newname:
            newname = path.basename(fname)
        desc = dlg.fileDesc.text()
        filecontent = open(fname, 'rb').read()
        remotefn = self.client.ask('transfer', filecontent)
        if remotefn is not None:
            self.client.eval('_LogAttach(%r, [%r], [%r])' %
                             (desc, remotefn, newname))
        self.timer.start(750)

    @pyqtSlot()
    def on_actionPrint_triggered(self):
        # Let the user select the desired printer via the system printer list
        printer = QPrinter()
        dialog = QPrintDialog(printer)

        if not dialog.exec_():
            return

        mainFrame = self.preview.page().mainFrame()
        childFrames = mainFrame.childFrames()

        # Workaround for Qt versions < 4.8.0
        printWholeSite = True
        if hasattr(QWebView, 'selectedHtml'):
            if self.preview.hasSelection():
                printWholeSite = False

        # use whole frame if no content is selected or selecting html is not
        # supported
        if printWholeSite:
            # set 'content' frame active as printing an inactive web frame
            # doesn't work properly

            if len(childFrames) >= 2:
                childFrames[1].setFocus()

                # thanks to setFocus, we can get the print the frame
                # with evaluated javascript
                html = childFrames[1].toHtml()
        else:
            html = self.preview.selectedHtml()

            # construct head
            head = '<head>'

            # extract head from child frames
            for frame in childFrames:
                headEl = frame.findFirstElement('head')
                head += headEl.toInnerXml()

            head += '</head>'

            # concat new head and selection
            # the result may be invalid html; needs improvements!
            html = head + html

        # prepend a header to the log book
        html.replace('</head>', '</head><h1>NICOS Log book</h1>')

        # let qt layout the content
        doc = QTextDocument()
        doc.setHtml(html)

        doc.print_(printer)
Ejemplo n.º 3
0
class LiveDataPanel(Panel):
    """Provides a generic "detector live view".

    For most instruments, a specific panel must be implemented that takes care
    of the individual live display needs.

    Options:

    * ``filetypes`` (default []) - List of filename extensions whose content
      should be displayed.
    * ``detectors`` (default []) - List of detector devices whose data should
      be displayed. If not set data from all configured detectors will be
      shown.
    * ``cachesize`` (default 20) - Number of entries in the live data cache.
      The live data cache allows displaying of previously measured data.
    * ``liveonlyindex`` (default None) - Enable live only view. This disables
      interaction with the liveDataPanel and only displays the dataset of the
      set index.

    * ``defaults`` (default []) - List of strings representing options to be
      set for every configured plot.
      These options can not be set on a per plot basis since they are global.
      Options are as follows:

        * ``logscale`` - Switch the logarithic scale on.
        * ``center`` - Display the center lines for the image.
        * ``nolines`` - Display lines for the curve.
        * ``markers`` - Display symbols for data points.
        * ``unzoom`` - Unzoom the plot when new data is received.

    * ``plotsettings`` (default []) - List of dictionaries which contain
      settings for the individual datasets.

      Each entry will be applied to one of the detector's datasets.

      * ``plotcount`` (default 1) - Amount of plots in the dataset.
      * ``marks`` (default 'omark') - Shape of the markers (if displayed).
        Possible values are:

          'dot', 'plus', 'asterrisk', 'circle', 'diagonalcross', 'solidcircle',
          'triangleup', 'solidtriangleup', 'triangledown', 'solidtriangledown',
          'square', 'solidsquare', 'bowtie', 'solidbowtie', 'hourglass',
          'solidhourglass', 'diamond', 'soliddiamond', 'star', 'solidstar',
          'triupdown', 'solidtriright', 'solidtrileft', 'hollowplus',
          'solidplus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'star4',
          'star5', 'star6', 'star7', 'star8', 'vline', 'hline', 'omark'

      * ``markersize`` (default 1) - Size of the markers (if displayed).
      * ``offset`` (default 0) - Offset for the X axis labels of
        each curve in 1D plots.
      * ``colors`` (default ['blue']) - Color of the marks and lines
        (if displayed).
        If colors are set as a list the colors will be applied to the
        individual plots (and default back to blue when wrong/missing),
        for example:

        ['red', 'green']: The first plot will be red, the second green and
        the others will be blue (default).

        'red': all plots will be red.
    """

    panelName = 'Live data view'

    ui = f'{uipath}/panels/live.ui'

    def __init__(self, parent, client, options):
        Panel.__init__(self, parent, client, options)
        loadUi(self, self.ui)

        self._allowed_filetypes = set()
        self._allowed_detectors = set()
        self._runtime = 0
        self._range_active = False
        self._cachesize = 20
        self._livewidgets = {}  # livewidgets for rois: roi_key -> widget
        self._fileopen_filter = None
        self.widget = None
        self.menu = None
        self.unzoom = False
        self.lastSettingsIndex = None
        self._axis_labels = {}
        self.params = {}
        self._offset = 0

        self.statusBar = QStatusBar(self, sizeGripEnabled=False)
        policy = self.statusBar.sizePolicy()
        policy.setVerticalPolicy(QSizePolicy.Fixed)
        self.statusBar.setSizePolicy(policy)
        self.statusBar.setSizeGripEnabled(False)
        self.layout().addWidget(self.statusBar)

        self.toolbar = QToolBar('Live data')
        self.toolbar.addAction(self.actionOpen)
        self.toolbar.addAction(self.actionPrint)
        self.toolbar.addAction(self.actionSavePlot)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.actionLogScale)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.actionKeepRatio)
        self.toolbar.addAction(self.actionUnzoom)
        self.toolbar.addAction(self.actionColormap)
        self.toolbar.addAction(self.actionMarkCenter)
        self.toolbar.addAction(self.actionROI)

        self._actions2D = [self.actionROI, self.actionColormap]
        self.setControlsEnabled(False)
        self.set2DControlsEnabled(False)

        # hide fileselection in liveonly mode
        self._liveOnlyIndex = options.get('liveonlyindex', None)
        if self._liveOnlyIndex is not None:
            self.pastFilesWidget.hide()
            self.statusBar.hide()
            # disable interactions with the plot
            self.setAttribute(Qt.WA_TransparentForMouseEvents)

        self.liveitems = []
        self.setLiveItems(1)
        self._livechannel = 0

        self.splitter.setSizes([20, 80])
        self.splitter.restoreState(self.splitterstate)

        if hasattr(self.window(), 'closed'):
            self.window().closed.connect(self.on_closed)
        client.livedata.connect(self.on_client_livedata)
        client.connected.connect(self.on_client_connected)
        client.cache.connect(self.on_cache)

        self.rois = {}
        self.detectorskey = None
        # configure allowed file types
        supported_filetypes = ReaderRegistry.filetypes()
        opt_filetypes = set(options.get('filetypes', supported_filetypes))
        self._allowed_filetypes = opt_filetypes & set(supported_filetypes)

        # configure allowed detector device names
        detectors = options.get('detectors')
        if detectors:
            self._allowed_detectors = set(detectors)

        defaults = options.get('defaults', [])

        if 'logscale' in defaults:
            self.actionLogScale.setChecked(True)
        if 'center' in defaults:
            self.actionMarkCenter.setChecked(True)
        if 'nolines' not in defaults:
            self.actionLines.setChecked(True)
        if 'markers' in defaults:
            self.actionSymbols.setChecked(True)
        if 'unzoom' in defaults:
            self.unzoom = True

        self.plotsettings = options.get('plotsettings', [DEFAULTS])

        # configure caching
        self._cachesize = options.get('cachesize', self._cachesize)
        if self._cachesize < 1 or self._liveOnlyIndex is not None:
            self._cachesize = 1  # always cache the last live image
        self._datacache = BoundedOrderedDict(maxlen=self._cachesize)

        self._initControlsGUI()

    def _initControlsGUI(self):
        pass

    def setLiveItems(self, n):
        nitems = len(self.liveitems)
        if n < nitems:
            nfiles = self.fileList.count()
            for i in range(nitems - 1, n - 1, -1):
                self.liveitems.pop(i)
                self.fileList.takeItem(nfiles - nitems + i)
            if self._livechannel > n:
                self._livechannel = 0 if n > 0 else None
        else:
            for i in range(nitems, n):
                item = QListWidgetItem('<Live #%d>' % (i + 1))
                item.setData(FILENAME, i)
                item.setData(FILETYPE, '')
                item.setData(FILETAG, LIVE)
                self.fileList.insertItem(self.fileList.count(), item)
                self.liveitems.append(item)
            if self._liveOnlyIndex is not None:
                self.fileList.setCurrentRow(self._liveOnlyIndex)
        if n == 1:
            self.liveitems[0].setText('<Live>')
        else:
            self.liveitems[0].setText('<Live #1>')

    def set2DControlsEnabled(self, flag):
        if flag != self.actionKeepRatio.isChecked():
            self.actionKeepRatio.trigger()
        for action in self._actions2D:
            action.setVisible(flag)

    def setControlsEnabled(self, flag):
        for action in self.toolbar.actions():
            action.setEnabled(flag)
        self.actionOpen.setEnabled(True)  # File Open action always available

    def initLiveWidget(self, widgetcls):
        if isinstance(self.widget, widgetcls):
            return

        # delete the old widget
        if self.widget:
            self.widgetLayout.removeWidget(self.widget)
            self.widget.deleteLater()

        # create a new one
        self.widget = widgetcls(self)

        # enable/disable controls and set defaults for new livewidget instances
        self.setControlsEnabled(True)
        if isinstance(self.widget, LiveWidget1D):
            self.set2DControlsEnabled(False)
        else:
            self.set2DControlsEnabled(True)

        # apply current global settings
        self.widget.setCenterMark(self.actionMarkCenter.isChecked())
        self.widget.logscale(self.actionLogScale.isChecked())
        if isinstance(self.widget, LiveWidget1D):
            self.widget.setSymbols(self.actionSymbols.isChecked())
            self.widget.setLines(self.actionLines.isChecked())
        # liveonly mode does not display a status bar
        if self._liveOnlyIndex is None:
            self.widget.gr.cbm.addHandler(MouseEvent.MOUSE_MOVE,
                                          self.on_mousemove_gr)

        # handle menus
        self.menuColormap = QMenu(self)
        self.actionsColormap = QActionGroup(self)
        activeMap = self.widget.getColormap()
        activeCaption = None
        for name, value in COLORMAPS.items():
            caption = name.title()
            action = self.menuColormap.addAction(caption)
            action.setData(caption)
            action.setCheckable(True)
            if activeMap == value:
                action.setChecked(True)
                # update toolButton text later otherwise this may fail
                # depending on the setup and qt versions in use
                activeCaption = caption
            self.actionsColormap.addAction(action)
            action.triggered.connect(self.on_colormap_triggered)
        self.actionColormap.setMenu(self.menuColormap)

        # finish initiation
        self.widgetLayout.addWidget(self.widget)
        if activeCaption:
            self.toolbar.widgetForAction(
                self.actionColormap).setText(activeCaption)
        detectors = self.client.eval('session.experiment.detectors', [])
        self._register_rois(detectors)

    def loadSettings(self, settings):
        self.splitterstate = settings.value('splitter', '', QByteArray)

    def saveSettings(self, settings):
        settings.setValue('splitter', self.splitter.saveState())
        settings.setValue('geometry', self.saveGeometry())

    def getMenus(self):
        if self._liveOnlyIndex is not None:
            return []

        if not self.menu:
            menu = QMenu('&Live data', self)
            menu.addAction(self.actionOpen)
            menu.addAction(self.actionPrint)
            menu.addAction(self.actionSavePlot)
            menu.addSeparator()
            menu.addAction(self.actionKeepRatio)
            menu.addAction(self.actionUnzoom)
            menu.addAction(self.actionLogScale)
            menu.addAction(self.actionColormap)
            menu.addAction(self.actionMarkCenter)
            menu.addAction(self.actionROI)
            menu.addAction(self.actionSymbols)
            menu.addAction(self.actionLines)
            self.menu = menu
        return [self.menu]

    def _get_all_widgets(self):
        yield self.widget
        yield from self._livewidgets.values()

    def getToolbars(self):
        if self._liveOnlyIndex is not None:
            return []

        return [self.toolbar]

    def on_mousemove_gr(self, event):
        xyz = None
        if event.getWindow():  # inside plot
            xyz = self.widget.getZValue(event)
        if xyz:
            fmt = '(%g, %g)'  # x, y data 1D integral plots
            if len(xyz) == 3:
                fmt += ': %g'  # x, y, z data for 2D image plot
            self.statusBar.showMessage(fmt % xyz)
        else:
            self.statusBar.clearMessage()

    def on_actionColormap_triggered(self):
        w = self.toolbar.widgetForAction(self.actionColormap)
        m = self.actionColormap.menu()
        if m:
            m.popup(w.mapToGlobal(QPoint(0, w.height())))

    def on_colormap_triggered(self):
        action = self.actionsColormap.checkedAction()
        name = action.data()
        for widget in self._get_all_widgets():
            widget.setColormap(COLORMAPS[name.upper()])
        self.toolbar.widgetForAction(self.actionColormap).setText(name.title())

    @pyqtSlot()
    def on_actionLines_triggered(self):
        if self.widget and isinstance(self.widget, LiveWidget1D):
            self.widget.setLines(self.actionLines.isChecked())

    @pyqtSlot()
    def on_actionSymbols_triggered(self):
        if self.widget and isinstance(self.widget, LiveWidget1D):
            self.widget.setSymbols(self.actionSymbols.isChecked())

    def _getLiveWidget(self, roi):
        return self._livewidgets.get(roi + '/roi', None)

    def showRoiWindow(self, roikey):
        key = roikey + '/roi'
        widget = self._getLiveWidget(roikey)
        region = self.widget._rois[key]
        if not widget:
            widget = LiveWidget(None)
            widget.setWindowTitle(roikey)
            widget.setColormap(self.widget.getColormap())
            widget.setCenterMark(self.actionMarkCenter.isChecked())
            widget.logscale(self.actionLogScale.isChecked())
            widget.gr.setAdjustSelection(False)  # don't use adjust on ROIs
            for name, roi in self.rois.items():
                widget.setROI(name, roi)
            width = max(region.x) - min(region.x)
            height = max(region.y) - min(region.y)
            if width > height:
                dwidth = 500
                dheight = 500 * height // width
            else:
                dheight = 500
                dwidth = 500 * width // height
            widget.resize(dwidth, dheight)
            widget.closed.connect(self.on_roiWindowClosed)
        widget.setWindowForRoi(region)
        widget.update()
        widget.show()
        widget.activateWindow()
        self._livewidgets[key] = widget

    def closeRoiWindow(self, roi):
        widget = self._getLiveWidget(roi)
        if widget:
            widget.close()

    def on_closed(self):
        for w in self._livewidgets.values():
            w.close()

    def _register_rois(self, detectors):
        self.rois.clear()
        self.actionROI.setVisible(False)
        self.menuROI = QMenu(self)
        self.actionsROI = QActionGroup(self)
        self.actionsROI.setExclusive(False)
        for detname in detectors:
            self.log.debug('checking rois for detector \'%s\'', detname)
            for tup in self.client.eval(detname + '.postprocess', ''):
                roi = tup[0]
                cachekey = roi + '/roi'
                # check whether or not this is a roi (cachekey exists).
                keyval = self.client.getCacheKey(cachekey)
                if keyval:
                    self.on_roiChange(cachekey, keyval[1])
                    self.log.debug('register roi: %s', roi)
                    # create roi menu
                    action = self.menuROI.addAction(roi)
                    action.setData(roi)
                    action.setCheckable(True)
                    self.actionsROI.addAction(action)
                    action.triggered.connect(self.on_roi_triggered)
                    self.actionROI.setMenu(self.menuROI)
                    self.actionROI.setVisible(True)

    def on_actionROI_triggered(self):
        w = self.toolbar.widgetForAction(self.actionROI)
        self.actionROI.menu().popup(w.mapToGlobal(QPoint(0, w.height())))

    def on_roi_triggered(self):
        action = self.sender()
        roi = action.data()
        if action.isChecked():
            self.showRoiWindow(roi)
        else:
            self.closeRoiWindow(roi)

    def on_roiWindowClosed(self):
        widget = self.sender()
        if widget:
            key = None
            for key, w in self._livewidgets.items():
                if w == widget:
                    self.log.debug('delete roi: %s', key)
                    del self._livewidgets[key]
                    break
            if key:
                roi = key.rsplit('/', 1)[0]
                for action in self.actionsROI.actions():
                    if action.data() == roi:
                        action.setChecked(False)
                        self.log.debug('uncheck roi: %s', roi)

    def on_roiChange(self, key, value):
        self.log.debug('on_roiChange: %s %s', key, (value, ))
        self.rois[key] = value
        for widget in self._get_all_widgets():
            widget.setROI(key, value)
        widget = self._livewidgets.get(key, None)
        if widget:
            widget.setWindowForRoi(self.widget._rois[key])

    def on_cache(self, data):
        _time, key, _op, svalue = data
        try:
            value = cache_load(svalue)
        except ValueError:
            value = None
        if key in self.rois:
            self.on_roiChange(key, value)
        elif key == self.detectorskey and self.widget:
            self._register_rois(value)

    def on_client_connected(self):
        self.client.tell('eventunmask', ['livedata'])
        self.detectorskey = (self.client.eval('session.experiment.name') +
                             '/detlist').lower()

    def normalizeType(self, dtype):
        normalized_type = numpy.dtype(dtype).str
        if normalized_type not in DATATYPES:
            self.log.warning('Unsupported live data format: %s',
                             normalized_type)
            return
        return normalized_type

    def getIndexedUID(self, idx):
        return str(self.params['uid']) + '-' + str(idx)

    def _process_axis_labels(self, blobs):
        """Convert the raw axis label descriptions.
        tuple: `from, to`: Distribute labels equidistantly between the two
                           values.
        numbertype: `index into labels`: Actual labels are provided.
                                         Value is the starting index.
                                         Extract from first available blob.
                                         Remove said blob from list.
        None: `default`: Start at 0 with stepwidth 1.

        Save the axis labels to the datacache.
        """

        CLASSIC = {'define': 'classic'}

        for i, datadesc in enumerate(self.params['datadescs']):
            labels = {}
            titles = {}
            for size, axis in zip(reversed(datadesc['shape']), AXES):
                # if the 'labels' key does not exist or does not have the right
                # axis key set default to 'classic'.
                label = datadesc.get('labels', {
                    'x': CLASSIC,
                    'y': CLASSIC
                }).get(axis, CLASSIC)

                if label['define'] == 'range':
                    start = label.get('start', 0)
                    size = label.get('length', 1)
                    step = label.get('step', 1)
                    end = start + step * size
                    labels[axis] = numpy.arange(start, end, step)
                elif label['define'] == 'array':
                    index = label.get('index', 0)
                    labels[axis] = numpy.frombuffer(blobs[index],
                                                    label.get('dtype', '<i4'))
                else:
                    labels[axis] = self.getDefaultLabels(size)
                labels[axis] += self._offset if axis == 'x' else 0
                titles[axis] = label.get('title')

            # save the labels in the datacache with uid as key
            uid = self.getIndexedUID(i)
            if uid not in self._datacache:
                self._datacache[uid] = {}

            self._datacache[uid]['labels'] = labels
            self._datacache[uid]['titles'] = titles

    def _process_livedata(self, data, idx):
        # ignore irrelevant data in liveOnly mode
        if self._liveOnlyIndex is not None and idx != self._liveOnlyIndex:
            return

        try:
            descriptions = self.params['datadescs']
        except KeyError:
            self.log.warning('Livedata with tag "Live" without '
                             '"datadescs" provided.')
            return

        # pylint: disable=len-as-condition
        if len(data):
            # we got live data with specified formats
            arrays = self.processDataArrays(
                idx, numpy.frombuffer(data, descriptions[idx]['dtype']))

            if arrays is None:
                return

            # put everything into the cache
            uid = self.getIndexedUID(idx)
            self._datacache[uid]['dataarrays'] = arrays

            self.liveitems[idx].setData(FILEUID, uid)

    def _process_filenames(self):
        # TODO: allow multiple fileformats?
        #       would need to modify input from DemonSession.notifyDataFile

        number_of_items = self.fileList.count()
        for i, filedesc in enumerate(self.params['filedescs']):
            uid = self.getIndexedUID(number_of_items + i)
            name = filedesc['filename']
            filetype = filedesc.get('fileformat')
            if filetype is None or filetype not in ReaderRegistry.filetypes():
                continue  # Ignore unregistered file types
            self.add_to_flist(name, filetype, FILE, uid)
            try:
                # update display for selected live channel,
                # just cache otherwise
                self.setDataFromFile(name,
                                     filetype,
                                     uid,
                                     display=(i == self._livechannel))
            except Exception as e:
                if uid in self._datacache:
                    # image is already cached
                    # suppress error message for cached image
                    self.log.debug(e)
                else:
                    # image is not cached and could not be loaded
                    self.log.exception(e)

    def on_client_livedata(self, params, blobs):
        # blobs is a list of data blobs and labels blobs
        if self._allowed_detectors \
                and params['det'] not in self._allowed_detectors:
            return

        self.params = params
        self._runtime = params['time']
        if params['tag'] == LIVE:
            datacount = len(params['datadescs'])
            self.setLiveItems(datacount)

            self._process_axis_labels(blobs[datacount:])

            for i, blob in enumerate(blobs[:datacount]):
                self._process_livedata(blob, i)
            if not datacount:
                self._process_livedata([], 0)
        elif params['tag'] == FILE:
            self._process_filenames()

        self._show()

    def getDefaultLabels(self, size):
        return numpy.array(range(size))

    def convertLabels(self, labelinput):
        """Convert the input into a processable format"""

        for i, entry in enumerate(labelinput):
            if isinstance(entry, str):
                labelinput[i] = self.normalizeType(entry)

        return labelinput

    def _initLiveWidget(self, array):
        """Initialize livewidget based on array's shape"""
        if len(array.shape) == 1:
            widgetcls = LiveWidget1D
        else:
            widgetcls = IntegralLiveWidget
        self.initLiveWidget(widgetcls)

    def setDataFromFile(self, filename, filetype, uid=None, display=True):
        """Load data array from file and dispatch to live widgets using
        ``setData``. Do not use caching if uid is ``None``.
        """
        array = readDataFromFile(filename, filetype)
        if array is not None:
            if uid:
                if uid not in self._datacache:
                    self.log.debug('add to cache: %s', uid)
                self._datacache[uid] = {}
                self._datacache[uid]['dataarrays'] = [array]
            if display:
                self._initLiveWidget(array)
                for widget in self._get_all_widgets():
                    widget.setData(array)


#           self.setData([array], uid, display=display)
            return array.shape
        else:
            raise NicosError('Cannot read file %r' % filename)

    def processDataArrays(self, index, entry):
        """Check if the input 1D array has the expected amount of values.
        If the array is too small an Error is raised.
        If the size exceeds the expected amount it is truncated.

        Returns a list of arrays corresponding to the ``plotcount`` of
        ``index`` into ``datadescs`` of the current params"""

        datadesc = self.params['datadescs'][index]
        count = datadesc.get('plotcount', DEFAULTS['plotcount'])
        shape = datadesc['shape']

        # ignore irrelevant data in liveOnly mode
        if self._liveOnlyIndex is not None and index != self._liveOnlyIndex:
            return

        # determine 1D array size
        arraysize = numpy.product(shape)

        # check and split the input array
        if len(entry) < count * arraysize:
            self.log.warning('Expected dataarray with %d entries, got %d',
                             count * arraysize, len(entry))
            return
        arrays = numpy.split(entry[:count * arraysize], count)

        # reshape every array in the list
        for i, array in enumerate(arrays):
            arrays[i] = array.reshape(shape)
        return arrays

    def applyPlotSettings(self):
        if not self.widget or not isinstance(self.widget, LiveWidget1D):
            return

        if self._liveOnlyIndex is not None:
            index = self._liveOnlyIndex
        elif self.fileList.currentItem() not in self.liveitems:
            return
        else:
            index = self.fileList.currentRow()

        if isinstance(self.widget, LiveWidget1D):

            def getElement(l, index, default):
                try:
                    return l[index]
                except IndexError:
                    return default

            settings = getElement(self.plotsettings, index, DEFAULTS)

            if self.params['tag'] == LIVE:
                plotcount = self.params['datadescs'][index].get(
                    'plotcount', DEFAULTS['plotcount'])
            else:
                plotcount = DEFAULTS['plotcount']
            marks = [settings.get('marks', DEFAULTS['marks'])]
            markersize = settings.get('markersize', DEFAULTS['markersize'])
            offset = settings.get('offset', DEFAULTS['offset'])
            colors = settings.get('colors', DEFAULTS['colors'])

            if isinstance(colors, list):
                if len(colors) > plotcount:
                    colors = colors[:plotcount]
                while len(colors) < plotcount:
                    colors.append(DEFAULTS['colors'])
            else:
                colors = [colors] * plotcount

            self.setOffset(offset)
            self.widget.setMarks(marks)
            self.widget.setMarkerSize(markersize)
            self.widget.setPlotCount(plotcount, colors)

    def setOffset(self, offset):
        self._offset = offset

    def getDataFromItem(self, item):
        """Extract and return the data associated with the item.
        If the data is in the cache return it.
        If the data is in a valid file extract it from there.
        """

        if item is None:
            return

        uid = item.data(FILEUID)
        # data is cached
        if uid and hasattr(self, '_datacache') and uid in self._datacache:
            return self._datacache[uid]
        # cache has cleared data or data has not been cached in the first place
        elif uid is None and item.data(FILETAG) == FILE:
            filename = item.data(FILENAME)
            filetype = item.data(FILETYPE)

            if path.isfile(filename):
                rawdata = readDataFromFile(filename, filetype)
                labels = {}
                titles = {}
                for axis, entry in zip(AXES, reversed(rawdata.shape)):
                    labels[axis] = numpy.arange(entry)
                    titles[axis] = axis
                data = {
                    'labels': labels,
                    'titles': titles,
                    'dataarrays': [rawdata]
                }
                return data
            # else:
            # TODO: mark for deletion on item changed?

    def _show(self, data=None):
        """Show the provided data. If no data has been provided extract it
        from the datacache via the current item's uid.

        :param data: dictionary containing 'dataarrays' and 'labels'
        """

        idx = self.fileList.currentRow()
        if idx == -1:
            self.fileList.setCurrentRow(0)
            return

        # no data has been provided, try to get it from the cache
        if data is None:
            data = self.getDataFromItem(self.fileList.currentItem())
            # still no data
            if data is None:
                return

        arrays = data.get('dataarrays', [])
        labels = data.get('labels', {})
        titles = data.get('titles', {})

        # if multiple datasets have to be displayed in one widget, they have
        # the same dimensions, so we only need the dimensions of one set
        self._initLiveWidget(arrays[0])
        self.applyPlotSettings()
        for widget in self._get_all_widgets():
            widget.setData(arrays, labels)
            widget.setTitles(titles)

        if self.unzoom and self.widget:
            self.on_actionUnzoom_triggered()

    def remove_obsolete_cached_files(self):
        """Remove or flag items which are no longer cached.
        The cache will delete items if it's size exceeds ´cachesize´.
        This checks the items in the filelist and their caching status,
        removing items with deleted associated files and flagging items
        with valid files to be reloaded if selected by the user.
        """

        for index in reversed(range(self.fileList.count())):
            item = self.fileList.item(index)
            uid = item.data(FILEUID)
            # is the uid still cached
            if uid and uid not in self._datacache:
                # does the file still exist on the filesystem
                if path.isfile(item.data(FILENAME)):
                    item.setData(FILEUID, None)
                else:
                    self.fileList.takeItem(index)

    def add_to_flist(self, filename, filetype, tag, uid=None, scroll=True):
        # liveonly mode doesn't display a filelist
        if self._liveOnlyIndex is not None:
            return

        shortname = path.basename(filename)
        item = QListWidgetItem(shortname)
        item.setData(FILENAME, filename)
        item.setData(FILETYPE, filetype)
        item.setData(FILETAG, tag)
        item.setData(FILEUID, uid)
        self.fileList.insertItem(self.fileList.count(), item)
        if uid:
            self.remove_obsolete_cached_files()
        if scroll:
            self.fileList.scrollToBottom()
        return item

    def on_fileList_currentItemChanged(self):
        self._show()

    @pyqtSlot()
    def on_actionOpen_triggered(self):
        """Open image file using registered reader classes."""
        ftypes = {
            ffilter: ftype
            for ftype, ffilter in ReaderRegistry.filefilters()
            if not self._allowed_filetypes or ftype in self._allowed_filetypes
        }
        fdialog = FileFilterDialog(self, "Open data files", "",
                                   ";;".join(ftypes.keys()))
        if self._fileopen_filter:
            fdialog.selectNameFilter(self._fileopen_filter)
        if fdialog.exec_() != fdialog.Accepted:
            return
        files = fdialog.selectedFiles()
        if not files:
            return
        self._fileopen_filter = fdialog.selectedNameFilter()
        filetype = ftypes[self._fileopen_filter]
        errors = []

        def _cacheFile(fn, filetype):
            uid = uuid4()
            # setDataFromFile may raise an `NicosException`, e.g.
            # if the file cannot be opened.
            try:
                self.setDataFromFile(fn, filetype, uid, display=False)
            except Exception as err:
                errors.append('%s: %s' % (fn, err))
            else:
                return self.add_to_flist(fn, filetype, FILE, uid)

        # load and display first item
        f = files.pop(0)
        item = _cacheFile(f, filetype)
        if item is not None:
            self.fileList.setCurrentItem(item)
        cachesize = self._cachesize - 1
        # add first `cachesize` files to cache
        for _, f in enumerateWithProgress(files[:cachesize],
                                          "Loading data files...",
                                          parent=fdialog):
            _cacheFile(f, filetype)
        # add further files to file list (open on request/itemClicked)
        for f in files[cachesize:]:
            self.add_to_flist(f, filetype, FILE)

        if errors:
            self.showError('Some files could not be opened:\n\n' +
                           '\n'.join(errors))

    @pyqtSlot()
    def on_actionUnzoom_triggered(self):
        self.widget.unzoom()

    @pyqtSlot()
    def on_actionPrint_triggered(self):
        self.widget.printDialog()

    @pyqtSlot()
    def on_actionSavePlot_triggered(self):
        self.widget.savePlot()

    @pyqtSlot()
    def on_actionLogScale_triggered(self):
        for widget in self._get_all_widgets():
            widget.logscale(self.actionLogScale.isChecked())

    @pyqtSlot()
    def on_actionMarkCenter_triggered(self):
        flag = self.actionMarkCenter.isChecked()
        for widget in self._get_all_widgets():
            widget.setCenterMark(flag)

    @pyqtSlot()
    def on_actionKeepRatio_triggered(self):
        self.widget.gr.setAdjustSelection(self.actionKeepRatio.isChecked())
Ejemplo n.º 4
0
class ScriptStatusPanel(Panel):
    """Provides a view of the currently executed script.

    The current position within the script is shown with an arrow.  The panel
    also displays queued scripts.

    Options:

    * ``stopcounting`` (default False) -- Configure the stop button behaviour,
      if is set to ``True``, the execution of a script will be aborted,
      otherwise a counting will be finished first before the script will be
      stopped.
    * ``eta`` (default False) - if set to ``True`` the "ETA" (estimated time of
      end of script) will be displayed if the
      :class:`daemon <nicos.services.daemon.NicosDaemon>` is configured to run
      with automatic simulation of the current command.
    """

    panelName = 'Script status'

    SHOW_ETA_STATES =[
        'running',
        'paused'
    ]

    def __init__(self, parent, client, options):
        Panel.__init__(self, parent, client, options)
        loadUi(self, 'panels/status.ui')

        self.stopcounting = False
        self.menus = None
        self.bar = None
        self.queueFrame.hide()
        self.statusLabel.hide()
        self.pause_color = QColor('#ffdddd')
        self.idle_color = parent.user_color

        self.script_queue = ScriptQueue(self.queueFrame, self.queueView)
        self.current_line = -1
        self.current_request = {}
        self.curlineicon = QIcon(':/currentline')
        self.errlineicon = QIcon(':/errorline')
        empty = QPixmap(16, 16)
        empty.fill(Qt.transparent)
        self.otherlineicon = QIcon(empty)
        self.traceView.setItemDelegate(LineDelegate(24, self.traceView))

        self.stopcounting = bool(options.get('stopcounting', False))
        if self.stopcounting:
            tooltip = 'Aborts the current executed script'
            self.actionStop.setToolTip(tooltip)
            self.actionStop.setText('Abort current script')
            self.actionStop2.setToolTip(tooltip)

        self.showETA = bool(options.get('eta', False))
        self.etaWidget.hide()

        client.request.connect(self.on_client_request)
        client.processing.connect(self.on_client_processing)
        client.blocked.connect(self.on_client_blocked)
        client.status.connect(self.on_client_status)
        client.initstatus.connect(self.on_client_initstatus)
        client.disconnected.connect(self.on_client_disconnected)
        client.rearranged.connect(self.on_client_rearranged)
        client.updated.connect(self.on_client_updated)
        client.eta.connect(self.on_client_eta)

        bar = QToolBar('Script control')
        bar.setObjectName(bar.windowTitle())
        # unfortunately it is not wise to put a menu in its own dropdown menu,
        # so we have to duplicate the actionBreak and actionStop...
        dropdown1 = QMenu('', self)
        dropdown1.addAction(self.actionBreak)
        dropdown1.addAction(self.actionBreakCount)
        dropdown1.addAction(self.actionFinishEarly)
        self.actionBreak2.setMenu(dropdown1)
        dropdown2 = QMenu('', self)
        dropdown2.addAction(self.actionStop)
        dropdown2.addAction(self.actionFinish)
        dropdown2.addAction(self.actionFinishEarlyAndStop)
        self.actionStop2.setMenu(dropdown2)
        bar.addAction(self.actionBreak2)
        bar.addAction(self.actionContinue)
        bar.addAction(self.actionStop2)
        bar.addAction(self.actionEmergencyStop)
        self.bar = bar
        # self.mainwindow.addToolBar(bar)

        menu = QMenu('&Script control', self)
        menu.addAction(self.actionBreak)
        menu.addAction(self.actionBreakCount)
        menu.addAction(self.actionContinue)
        menu.addAction(self.actionFinishEarly)
        menu.addSeparator()
        menu.addAction(self.actionStop)
        menu.addAction(self.actionFinish)
        menu.addAction(self.actionFinishEarlyAndStop)
        menu.addSeparator()
        menu.addAction(self.actionEmergencyStop)
        self.mainwindow.menuBar().insertMenu(
            self.mainwindow.menuWindows.menuAction(), menu)

        self.activeGroup = QActionGroup(self)
        self.activeGroup.addAction(self.actionBreak)
        self.activeGroup.addAction(self.actionBreak2)
        self.activeGroup.addAction(self.actionBreakCount)
        self.activeGroup.addAction(self.actionContinue)
        self.activeGroup.addAction(self.actionStop)
        self.activeGroup.addAction(self.actionStop2)
        self.activeGroup.addAction(self.actionFinish)
        self.activeGroup.addAction(self.actionFinishEarly)
        self.activeGroup.addAction(self.actionFinishEarlyAndStop)

        self._status = 'idle'

    def setViewOnly(self, viewonly):
        self.activeGroup.setEnabled(not viewonly)

    def setCustomStyle(self, font, back):
        self.idle_color = back
        for widget in (self.traceView, self.queueView):
            widget.setFont(font)
            setBackgroundColor(widget, back)

    def getToolbars(self):
        return [self.bar]

    def getMenus(self):
        return []

    def updateStatus(self, status, exception=False):
        self._status = status

        isconnected = status != 'disconnected'
        self.actionBreak.setEnabled(isconnected and status != 'idle')
        self.actionBreak2.setEnabled(isconnected and status != 'idle')
        self.actionBreak2.setVisible(status != 'paused')
        self.actionBreakCount.setEnabled(isconnected and status != 'idle')
        self.actionContinue.setVisible(status == 'paused')
        self.actionStop.setEnabled(isconnected and status != 'idle')
        self.actionStop2.setEnabled(isconnected and status != 'idle')
        self.actionFinish.setEnabled(isconnected and status != 'idle')
        self.actionFinishEarly.setEnabled(isconnected and status != 'idle')
        self.actionFinishEarlyAndStop.setEnabled(isconnected and status != 'idle')
        self.actionEmergencyStop.setEnabled(isconnected)
        if status == 'paused':
            self.statusLabel.setText('Script is paused.')
            self.statusLabel.show()
            setBackgroundColor(self.traceView, self.pause_color)
        else:
            self.statusLabel.hide()
            setBackgroundColor(self.traceView, self.idle_color)
        self.traceView.update()

        if status == 'idle':
            self.etaWidget.hide()

    def setScript(self, script):
        self.traceView.clear()
        lines = script.splitlines()
        longest = len(str(len(lines)))
        padding = ' ' * (longest + 3)
        height = QFontMetrics(self.traceView.font()).height()
        for (i, line) in enumerate(lines):
            item = QListWidgetItem(self.otherlineicon, padding + line,
                                   self.traceView)
            item.setSizeHint(QSize(-1, height))
            item.setData(Qt.UserRole, '%*d |' % (longest, i+1))
            self.traceView.addItem(item)
        self.current_line = -1

    def setCurrentLine(self, line, error_exit=False):
        if self.current_line != -1:
            item = self.traceView.item(self.current_line - 1)
            if item:
                # when a script has exited with an error, keep indicating the
                # current line, with a red arrow
                if error_exit and line == -1:
                    item.setIcon(self.errlineicon)
                else:
                    item.setIcon(self.otherlineicon)
            self.current_line = -1
        if 0 < line <= self.traceView.count():
            item = self.traceView.item(line - 1)
            item.setIcon(self.curlineicon)
            self.traceView.scrollToItem(item)
            self.current_line = line

    def on_client_request(self, request):
        if 'script' not in request:
            return
        self.script_queue.append(request)

    def on_client_processing(self, request):
        if 'script' not in request:
            return
        new_current_line = -1
        if self.current_request['reqid'] == request['reqid']:
            # on update, set the current line to the same as before
            # (this may be WRONG, but should not in most cases, and it's
            # better than no line indicator at all)
            new_current_line = self.current_line
        self.script_queue.remove(request['reqid'])
        self.setScript(request['script'])
        self.current_request = request
        self.setCurrentLine(new_current_line)

    def on_client_blocked(self, requests):
        for reqid in requests:
            self.script_queue.remove(reqid)

    def on_client_eta(self, data):
        if not self.showETA or self._status not in self.SHOW_ETA_STATES:
            return

        state, eta = data

        if state == SIM_STATES['pending']:
            self.etaWidget.hide()
        elif state == SIM_STATES['running']:
            self.etaLabel.setText('Calculating...')
            self.etaWidget.show()
        elif state == SIM_STATES['success'] and eta > time():
            self.etaLabel.setText(formatEndtime(eta - time()))
            self.etaWidget.show()
        elif state == SIM_STATES['failed']:
            self.etaLabel.setText('Could not calculate ETA')
            self.etaWidget.show()

    def on_client_initstatus(self, state):
        self.setScript(state['script'])
        self.current_request['script'] = state['script']
        self.current_request['reqid'] = None
        self.on_client_status(state['status'])
        for req in state['requests']:
            self.on_client_request(req)
        if self.showETA:
            self.on_client_eta(state['eta'])

    def on_client_status(self, data):
        status, line = data
        if line != self.current_line:
            self.setCurrentLine(line, status == STATUS_IDLEEXC)

    def on_client_disconnected(self):
        self.script_queue.clear()

    def on_client_rearranged(self, items):
        self.script_queue.rearrange(items)

    def on_client_updated(self, request):
        if 'script' not in request:
            return
        self.script_queue.update(request)

    @pyqtSlot()
    def on_actionBreak_triggered(self):
        self.client.tell_action('break', BREAK_AFTER_STEP)

    @pyqtSlot()
    def on_actionBreak2_triggered(self):
        self.on_actionBreak_triggered()

    @pyqtSlot()
    def on_actionBreakCount_triggered(self):
        self.client.tell_action('break', BREAK_NOW)

    @pyqtSlot()
    def on_actionContinue_triggered(self):
        self.client.tell_action('continue')

    @pyqtSlot()
    def on_actionStop_triggered(self):
        if self.stopcounting:
            self.client.tell_action('stop', BREAK_NOW)
        else:
            self.client.tell_action('stop', BREAK_AFTER_STEP)

    @pyqtSlot()
    def on_actionStop2_triggered(self):
        self.on_actionStop_triggered()

    @pyqtSlot()
    def on_actionFinish_triggered(self):
        self.client.tell_action('stop', BREAK_AFTER_LINE)

    @pyqtSlot()
    def on_actionFinishEarly_triggered(self):
        self.client.tell_action('finish')

    @pyqtSlot()
    def on_actionFinishEarlyAndStop_triggered(self):
        self.client.tell_action('stop', BREAK_AFTER_STEP)
        self.client.tell_action('finish')

    @pyqtSlot()
    def on_actionEmergencyStop_triggered(self):
        self.client.tell_action('emergency')

    @pyqtSlot()
    def on_clearQueue_clicked(self):
        if self.client.tell('unqueue', '*'):
            self.script_queue.clear()

    @pyqtSlot()
    def on_deleteQueueItem_clicked(self):
        item = self.queueView.currentItem()
        if not item:
            return
        reqid = item.data(Qt.UserRole)
        if self.client.tell('unqueue', str(reqid)):
            self.script_queue.remove(reqid)

    def moveItem(self, delta):
        rowCount = self.queueView.count()
        IDs = []
        for i in range(rowCount):
            IDs.append(self.queueView.item(i).data(Qt.UserRole))
        curID = self.queueView.currentItem().data(Qt.UserRole)
        i = IDs.index(curID)
        IDs.insert(i + delta, IDs.pop(i))
        self.client.ask('rearrange', IDs)

    @pyqtSlot()
    def on_upButton_clicked(self):
        if self.queueView.currentItem():
            self.moveItem(-1)

    @pyqtSlot()
    def on_downButton_clicked(self):
        if self.queueView.currentItem():
            self.moveItem(+1)
Ejemplo n.º 5
0
    def __init__(self, parent, client, options):
        Panel.__init__(self, parent, client, options)
        loadUi(self, 'panels/status.ui')

        self.stopcounting = False
        self.menus = None
        self.bar = None
        self.queueFrame.hide()
        self.statusLabel.hide()
        self.pause_color = QColor('#ffdddd')
        self.idle_color = parent.user_color

        self.script_queue = ScriptQueue(self.queueFrame, self.queueView)
        self.current_line = -1
        self.current_request = {}
        self.curlineicon = QIcon(':/currentline')
        self.errlineicon = QIcon(':/errorline')
        empty = QPixmap(16, 16)
        empty.fill(Qt.transparent)
        self.otherlineicon = QIcon(empty)
        self.traceView.setItemDelegate(LineDelegate(24, self.traceView))

        self.stopcounting = bool(options.get('stopcounting', False))
        if self.stopcounting:
            tooltip = 'Aborts the current executed script'
            self.actionStop.setToolTip(tooltip)
            self.actionStop.setText('Abort current script')
            self.actionStop2.setToolTip(tooltip)

        self.showETA = bool(options.get('eta', False))
        self.etaWidget.hide()

        client.request.connect(self.on_client_request)
        client.processing.connect(self.on_client_processing)
        client.blocked.connect(self.on_client_blocked)
        client.status.connect(self.on_client_status)
        client.initstatus.connect(self.on_client_initstatus)
        client.disconnected.connect(self.on_client_disconnected)
        client.rearranged.connect(self.on_client_rearranged)
        client.updated.connect(self.on_client_updated)
        client.eta.connect(self.on_client_eta)

        bar = QToolBar('Script control')
        bar.setObjectName(bar.windowTitle())
        # unfortunately it is not wise to put a menu in its own dropdown menu,
        # so we have to duplicate the actionBreak and actionStop...
        dropdown1 = QMenu('', self)
        dropdown1.addAction(self.actionBreak)
        dropdown1.addAction(self.actionBreakCount)
        dropdown1.addAction(self.actionFinishEarly)
        self.actionBreak2.setMenu(dropdown1)
        dropdown2 = QMenu('', self)
        dropdown2.addAction(self.actionStop)
        dropdown2.addAction(self.actionFinish)
        dropdown2.addAction(self.actionFinishEarlyAndStop)
        self.actionStop2.setMenu(dropdown2)
        bar.addAction(self.actionBreak2)
        bar.addAction(self.actionContinue)
        bar.addAction(self.actionStop2)
        bar.addAction(self.actionEmergencyStop)
        self.bar = bar
        # self.mainwindow.addToolBar(bar)

        menu = QMenu('&Script control', self)
        menu.addAction(self.actionBreak)
        menu.addAction(self.actionBreakCount)
        menu.addAction(self.actionContinue)
        menu.addAction(self.actionFinishEarly)
        menu.addSeparator()
        menu.addAction(self.actionStop)
        menu.addAction(self.actionFinish)
        menu.addAction(self.actionFinishEarlyAndStop)
        menu.addSeparator()
        menu.addAction(self.actionEmergencyStop)
        self.mainwindow.menuBar().insertMenu(
            self.mainwindow.menuWindows.menuAction(), menu)

        self.activeGroup = QActionGroup(self)
        self.activeGroup.addAction(self.actionBreak)
        self.activeGroup.addAction(self.actionBreak2)
        self.activeGroup.addAction(self.actionBreakCount)
        self.activeGroup.addAction(self.actionContinue)
        self.activeGroup.addAction(self.actionStop)
        self.activeGroup.addAction(self.actionStop2)
        self.activeGroup.addAction(self.actionFinish)
        self.activeGroup.addAction(self.actionFinishEarly)
        self.activeGroup.addAction(self.actionFinishEarlyAndStop)

        self._status = 'idle'
Ejemplo n.º 6
0
    def __init__(self, parent, client, options):
        Panel.__init__(self, parent, client, options)
        loadUi(self, 'panels/editor.ui')

        self.window = parent
        self.custom_font = None
        self.custom_back = None

        self.mainwindow.codeGenerated.connect(self.on_codeGenerated)

        if not has_scintilla:
            self.actionComment.setEnabled(False)

        self.menus = None
        self.bar = None
        self.current_status = None
        self.simuuid = ''
        self.recentf_actions = []
        self.searchdlg = None
        self.menuRecent = QMenu('Recent files')

        self.menuToolsActions = []

        for fn in self.recentf:
            action = QAction(fn.replace('&', '&&'), self)
            action.setData(fn)
            action.triggered.connect(self.openRecentFile)
            self.recentf_actions.append(action)
            self.menuRecent.addAction(action)

        self.tabber = QTabWidget(self, tabsClosable=True, documentMode=True)
        self.tabber.currentChanged.connect(self.on_tabber_currentChanged)
        self.tabber.tabCloseRequested.connect(self.on_tabber_tabCloseRequested)

        self.toolconfig = options.get('tools')

        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        hlayout.addWidget(self.tabber)
        self.mainFrame.setLayout(hlayout)

        self.editors = []  # tab index -> editor
        self.filenames = {}  # editor -> filename
        self.watchers = {}  # editor -> QFileSystemWatcher
        self.currentEditor = None

        self.saving = False  # True while saving
        self.warnWidget.hide()

        self.simOutStack.setCurrentIndex(0)
        hdr = self.simRanges.header()
        if QT_VER == 4:
            hdr.setResizeMode(QHeaderView.ResizeToContents)
        else:
            hdr.setSectionResizeMode(QHeaderView.ResizeToContents)
        self.simPane.hide()

        self.splitter.restoreState(self.splitterstate)
        self.treeModel = QFileSystemModel()
        idx = self.treeModel.setRootPath('/')
        self.treeModel.setNameFilters(['*.py', '*.txt'])
        self.treeModel.setNameFilterDisables(False)  # hide them
        self.fileTree.setModel(self.treeModel)
        self.fileTree.header().hideSection(1)
        self.fileTree.header().hideSection(2)
        self.fileTree.header().hideSection(3)
        self.fileTree.header().hide()
        self.fileTree.setRootIndex(idx)
        if not options.get('show_browser', True):
            self.scriptsPane.hide()
        self.actionShowScripts = self.scriptsPane.toggleViewAction()
        self.actionShowScripts.setText('Show Script Browser')

        self.activeGroup = QActionGroup(self)
        self.activeGroup.addAction(self.actionRun)
        self.activeGroup.addAction(self.actionSimulate)
        self.activeGroup.addAction(self.actionUpdate)

        client.simmessage.connect(self.on_client_simmessage)
        client.simresult.connect(self.on_client_simresult)
        if client.isconnected:
            self.on_client_connected()
        client.connected.connect(self.on_client_connected)
        client.setup.connect(self.on_client_connected)
        client.cache.connect(self.on_client_cache)
        client.experiment.connect(self.on_client_experiment)

        if self.openfiles:
            for fn in self.openfiles:
                self.openFile(fn, quiet=True)
        else:
            self.newFile()
Ejemplo n.º 7
0
 def setPanelToolbar(self):
     bar = QToolBar('History viewer')
     bar.addAction(self.actionNew)
     bar.addAction(self.actionEditView)
     bar.addSeparator()
     bar.addAction(self.actionSavePlot)
     bar.addAction(self.actionPrint)
     bar.addAction(self.actionSaveData)
     bar.addSeparator()
     bar.addAction(self.actionUnzoom)
     bar.addAction(self.actionLogScale)
     bar.addSeparator()
     bar.addAction(self.actionAutoScale)
     bar.addAction(self.actionScaleX)
     bar.addAction(self.actionScaleY)
     bar.addSeparator()
     bar.addAction(self.actionResetView)
     bar.addAction(self.actionDeleteView)
     bar.addSeparator()
     bar.addAction(self.actionFitPeak)
     wa = QWidgetAction(bar)
     self.fitPickCheckbox = QCheckBox(bar)
     self.fitPickCheckbox.setText('Pick')
     self.fitPickCheckbox.setChecked(True)
     self.actionPickInitial.setChecked(True)
     self.fitPickCheckbox.toggled.connect(self.actionPickInitial.setChecked)
     self.actionPickInitial.toggled.connect(self.fitPickCheckbox.setChecked)
     layout = QHBoxLayout()
     layout.setContentsMargins(10, 0, 10, 0)
     layout.addWidget(self.fitPickCheckbox)
     frame = QFrame(bar)
     frame.setLayout(layout)
     wa.setDefaultWidget(frame)
     bar.addAction(wa)
     ag = QActionGroup(bar)
     ag.addAction(self.actionFitPeakGaussian)
     ag.addAction(self.actionFitPeakLorentzian)
     ag.addAction(self.actionFitPeakPV)
     ag.addAction(self.actionFitPeakPVII)
     ag.addAction(self.actionFitTc)
     ag.addAction(self.actionFitCosine)
     ag.addAction(self.actionFitSigmoid)
     ag.addAction(self.actionFitLinear)
     ag.addAction(self.actionFitExponential)
     wa = QWidgetAction(bar)
     self.fitComboBox = QComboBox(bar)
     for a in ag.actions():
         itemtext = a.text().replace('&', '')
         self.fitComboBox.addItem(itemtext)
         self.fitfuncmap[itemtext] = a
     self.fitComboBox.currentIndexChanged.connect(
         self.on__fitComboBox_currentIndexChanged)
     wa.setDefaultWidget(self.fitComboBox)
     bar.addAction(wa)
     bar.addSeparator()
     bar.addAction(self.actionFitArby)
     self.bar = bar
     self.actionFitLinear.trigger()
     return bar
Ejemplo n.º 8
0
    def createPanelToolbar(self):
        bar = QToolBar('Scans')
        bar.addAction(self.actionSavePlot)
        bar.addAction(self.actionPrint)
        bar.addSeparator()
        bar.addAction(self.actionXAxis)
        bar.addAction(self.actionYAxis)
        bar.addAction(self.actionNormalized)
        bar.addSeparator()
        bar.addAction(self.actionLogXScale)
        bar.addAction(self.actionLogScale)
        bar.addAction(self.actionUnzoom)
        bar.addSeparator()
        bar.addAction(self.actionAutoScale)
        bar.addAction(self.actionScaleX)
        bar.addAction(self.actionScaleY)
        bar.addAction(self.actionLegend)
        bar.addAction(self.actionErrors)
        bar.addAction(self.actionResetPlot)
        bar.addAction(self.actionDeletePlot)
        bar.addSeparator()
        bar.addAction(self.actionAutoDisplay)
        bar.addAction(self.actionCombine)

        fitbar = QToolBar('Scan fitting')
        fitbar.addAction(self.actionFitPeak)
        wa = QWidgetAction(fitbar)
        self.fitPickCheckbox = QCheckBox(fitbar)
        self.fitPickCheckbox.setText('Pick')
        self.fitPickCheckbox.setChecked(True)
        self.actionPickInitial.setChecked(True)
        self.fitPickCheckbox.toggled.connect(self.actionPickInitial.setChecked)
        self.actionPickInitial.toggled.connect(self.fitPickCheckbox.setChecked)
        layout = QHBoxLayout()
        layout.setContentsMargins(10, 0, 10, 0)
        layout.addWidget(self.fitPickCheckbox)
        frame = QFrame(fitbar)
        frame.setLayout(layout)
        wa.setDefaultWidget(frame)
        fitbar.addAction(wa)
        ag = QActionGroup(fitbar)
        ag.addAction(self.actionFitPeakGaussian)
        ag.addAction(self.actionFitPeakLorentzian)
        ag.addAction(self.actionFitPeakPV)
        ag.addAction(self.actionFitPeakPVII)
        ag.addAction(self.actionFitTc)
        ag.addAction(self.actionFitCosine)
        ag.addAction(self.actionFitSigmoid)
        ag.addAction(self.actionFitLinear)
        ag.addAction(self.actionFitExponential)
        wa = QWidgetAction(fitbar)
        self.fitComboBox = QComboBox(fitbar)
        for a in ag.actions():
            itemtext = a.text().replace('&', '')
            self.fitComboBox.addItem(itemtext)
            self.fitfuncmap[itemtext] = a
        self.fitComboBox.currentIndexChanged.connect(
            self.on_fitComboBox_currentIndexChanged)
        wa.setDefaultWidget(self.fitComboBox)
        fitbar.addAction(wa)
        fitbar.addSeparator()
        fitbar.addAction(self.actionFitArby)

        bars = [bar, fitbar]

        return bars
Ejemplo n.º 9
0
    def getMenus(self):
        if not self.menus:
            menu1 = QMenu('&Data plot', self)
            menu1.addAction(self.actionSavePlot)
            menu1.addAction(self.actionPrint)
            menu1.addAction(self.actionAttachElog)
            menu1.addSeparator()
            menu1.addAction(self.actionResetPlot)
            menu1.addAction(self.actionAutoDisplay)
            menu1.addAction(self.actionCombine)
            menu1.addAction(self.actionClosePlot)
            menu1.addAction(self.actionDeletePlot)
            menu1.addSeparator()
            menu1.addAction(self.actionXAxis)
            menu1.addAction(self.actionYAxis)
            menu1.addAction(self.actionNormalized)
            menu1.addSeparator()
            menu1.addAction(self.actionUnzoom)
            menu1.addAction(self.actionLogXScale)
            menu1.addAction(self.actionLogScale)
            menu1.addAction(self.actionAutoScale)
            menu1.addAction(self.actionScaleX)
            menu1.addAction(self.actionScaleY)
            menu1.addAction(self.actionLegend)
            menu1.addAction(self.actionErrors)
            menu1.addSeparator()

            menu2 = QMenu('Data &manipulation', self)
            menu2.addAction(self.actionModifyData)
            menu2.addSeparator()
            ag = QActionGroup(menu2)
            ag.addAction(self.actionFitPeakGaussian)
            ag.addAction(self.actionFitPeakLorentzian)
            ag.addAction(self.actionFitPeakPV)
            ag.addAction(self.actionFitPeakPVII)
            ag.addAction(self.actionFitTc)
            ag.addAction(self.actionFitCosine)
            ag.addAction(self.actionFitSigmoid)
            ag.addAction(self.actionFitLinear)
            ag.addAction(self.actionFitExponential)
            menu2.addAction(self.actionFitPeak)
            menu2.addAction(self.actionPickInitial)
            menu2.addAction(self.actionFitPeakGaussian)
            menu2.addAction(self.actionFitPeakLorentzian)
            menu2.addAction(self.actionFitPeakPV)
            menu2.addAction(self.actionFitPeakPVII)
            menu2.addAction(self.actionFitTc)
            menu2.addAction(self.actionFitCosine)
            menu2.addAction(self.actionFitSigmoid)
            menu2.addAction(self.actionFitLinear)
            menu2.addAction(self.actionFitExponential)
            menu2.addSeparator()
            menu2.addAction(self.actionFitArby)

            self.menus = [menu1, menu2]

        return self.menus
Ejemplo n.º 10
0
 def getMenus(self):
     menu = QMenu('&History viewer', self)
     menu.addAction(self.actionNew)
     menu.addSeparator()
     menu.addAction(self.actionSavePlot)
     menu.addAction(self.actionPrint)
     menu.addAction(self.actionAttachElog)
     menu.addAction(self.actionSaveData)
     menu.addSeparator()
     menu.addAction(self.actionEditView)
     menu.addAction(self.actionCloseView)
     menu.addAction(self.actionDeleteView)
     menu.addAction(self.actionResetView)
     menu.addSeparator()
     menu.addAction(self.actionLogScale)
     menu.addAction(self.actionAutoScale)
     menu.addAction(self.actionScaleX)
     menu.addAction(self.actionScaleY)
     menu.addAction(self.actionUnzoom)
     menu.addAction(self.actionLegend)
     menu.addAction(self.actionSymbols)
     menu.addAction(self.actionLines)
     ag = QActionGroup(menu)
     ag.addAction(self.actionFitPeakGaussian)
     ag.addAction(self.actionFitPeakLorentzian)
     ag.addAction(self.actionFitPeakPV)
     ag.addAction(self.actionFitPeakPVII)
     ag.addAction(self.actionFitTc)
     ag.addAction(self.actionFitCosine)
     ag.addAction(self.actionFitSigmoid)
     ag.addAction(self.actionFitLinear)
     ag.addAction(self.actionFitExponential)
     menu.addAction(self.actionFitPeak)
     menu.addAction(self.actionPickInitial)
     menu.addAction(self.actionFitPeakGaussian)
     menu.addAction(self.actionFitPeakLorentzian)
     menu.addAction(self.actionFitPeakPV)
     menu.addAction(self.actionFitPeakPVII)
     menu.addAction(self.actionFitTc)
     menu.addAction(self.actionFitCosine)
     menu.addAction(self.actionFitSigmoid)
     menu.addAction(self.actionFitLinear)
     menu.addAction(self.actionFitExponential)
     menu.addSeparator()
     menu.addAction(self.actionFitArby)
     menu.addSeparator()
     menu.addAction(self.actionClose)
     self._refresh_presets()
     return [menu, self.presetmenu]