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()
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)
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())
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)
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 __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()
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
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
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
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]