class ScriptQueue(object): def __init__(self, frame, view): self._id2item = {} # mapping from request ID to list widget item self._frame = frame self._view = view self._timer = QTimer(singleShot=True, timeout=self._timeout) def _format_item(self, request): script = request['script'] if len(script) > 100: return script[:100] + '...' return script def _timeout(self): self._frame.show() def append(self, request): item = QListWidgetItem(self._format_item(request)) item.setData(Qt.UserRole, request['reqid']) self._id2item[request['reqid']] = item self._view.addItem(item) # delay showing the frame for 20 msecs, so that it doesn't flicker in # and out if the script is immediately taken out of the queue again self._timer.start(20) def update(self, request): item = self._id2item.get(request['reqid']) if item: text = self._format_item(request) item.setText(text) def remove(self, reqid): item = self._id2item.pop(reqid, None) if item is None: return item = self._view.takeItem(self._view.row(item)) if not self._id2item: self._timer.stop() self._frame.hide() return item def rearrange(self, reqids): selected = self._view.currentItem() for i in range(self._view.count()-1, -1, -1): self._view.takeItem(i) for reqid in reqids: self._view.addItem(self._id2item[reqid]) if selected: self._view.setCurrentItem(selected) def clear(self): self._frame.hide() self._view.clear() self._id2item.clear() # pylint: disable=nonzero-method def __nonzero__(self): return bool(self._id2item) __bool__ = __nonzero__
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/expinfo.ui') for ch in self.findChildren(NicosWidget): ch.setClient(client) client.setup.connect(self.on_client_setup) client.initstatus.connect(self.on_client_initstatus) self.detLabel.setFormatCallback( lambda value, strvalue: ', '.join(sorted(value))) self.envLabel.setFormatCallback( lambda value, strvalue: ', '.join(sorted(value))) self._sample_panel = options.get('sample_panel', GenericSamplePanel) self._new_exp_panel = options.get('new_exp_panel') self._finish_exp_panel = options.get('finish_exp_panel') self._timeout = options.get('popup_proposal_after', 0) if self._timeout: self._proposal_popup_timer = QTimer(interval=self._timeout * 3600000) self._proposal_popup_timer.setSingleShot(True) self._proposal_popup_timer.timeout.connect( self.on_proposal_popup_timer_timeout) else: self._proposal_popup_timer = None
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 startStartupTools(window, config): """Start all tools that are set to *runatstartup* from *config*. Use *window* as the parent window for dialogs. """ for tconfig in config: if isinstance(tconfig, menu): startStartupTools(window, tconfig.items) elif isinstance(tconfig, tool) and tconfig.options.get('runatstartup'): QTimer.singleShot(0, lambda tc=tconfig: runTool(window, tc))
def __init__(self, parent, _client, **_kwds): QMainWindow.__init__(self, parent) DlgUtils.__init__(self, 'Hexapod') # set during actions that will call signal handlers self.recursive = False loadUi(self, findResource('nicos_mlz/kws1/gui/tools/hexapod.ui')) for but in (self.butStart, self.butSetWorkspace, self.butSetFrame, self.butSaveVel): but.setEnabled(False) self.axes = {} self.axeslist = [] try: self._controller = PyTango.DeviceProxy(TANGO_DEV_BASE + 'controller') # make sure the server is running and create remaining proxies try: self._controller.State() except AttributeError: raise Exception('server appears to be not running') for axis in AXES: self.axes[axis] = PyTango.DeviceProxy(TANGO_DEV_BASE + axis) self.axeslist.append(self.axes[axis]) except Exception as err: self.showError('could not connect to tango server: %s' % err) self.deleteLater() return self.on_cbsWorkspace_activated(0) self.on_cbsFrame_activated(self.cbsFrame.currentText()) tx_speed = self.query_attr(self.axes['tx'], 'speed') self.inpVelTrans.setValue(tx_speed) self.lblVelTrans.setText(self.inpVelTrans.text()) self.inpVelRot.setValue(self.query_attr(self.axes['rx'], 'speed')) self.lblVelRot.setText(self.inpVelRot.text()) self.inpVelOmega.setValue(self.query_attr(self.axes['omega'], 'speed')) self.lblVelOmega.setText(self.inpVelOmega.text()) # ramp time = speed / acceleration self.inpRampUp.setValue(tx_speed / self.query_attr(self.axes['tx'], 'accel')) self.lblRampUp.setText(self.inpRampUp.text()) self.inpRampDown.setValue(tx_speed / self.query_attr(self.axes['tx'], 'decel')) self.lblRampDown.setText(self.inpRampDown.text()) self.updTimer = QTimer() self.updTimer.timeout.connect(self.updateTimer) self.updTimer.start(1000)
def __init__(self, client, watcher, entry, shortKey, showTimeStamp, showTTL, parent=None): base_class.__init__(self, parent) self.setupUi(self) self.updateTimer = QTimer(self) self.updateTimer.setSingleShot(True) self.watcher = watcher self.client = client self.entry = entry self.widgetValue = None self.setupEvents() self.setupWidgetUi(shortKey, showTimeStamp, showTTL)
def _label_entered(self, widget, event, from_mouse=True): infotext = '%s = %s' % (self.props['name'] or self.props['dev'] or self.props['key'], self.valuelabel.text()) if self.props['unit'].strip(): infotext += ' %s' % self.props['unit'] if self.props['statuskey']: try: const, msg = self._laststatus except ValueError: const, msg = self._laststatus, '' infotext += ', status is %s: %s' % (statuses.get(const, '?'), msg) infotext += ', changed %s ago' % ( nicedelta(currenttime() - self._lastchange)) self.widgetInfo.emit(infotext) if from_mouse: self._mousetimer = QTimer(self, timeout=lambda: self._label_entered(widget, event, False) ) self._mousetimer.start(1000)
def on_devValueChange(self, dev, value, strvalue, unitvalue, expired): # check expired values self._expired = expired self._lastvalue = value self._lastchange = currenttime() if self.props['maxlen'] > -1: self.valuelabel.setText(strvalue[:self.props['maxlen']]) else: self.valuelabel.setText(strvalue) if self._expired: setBothColors(self.valuelabel, (self._colorscheme['fore'][UNKNOWN], self._colorscheme['expired'])) if self.props['showExpiration']: self.valuelabel.setText(NOT_AVAILABLE) elif not self.props['istext']: setBothColors(self.valuelabel, (self._colorscheme['fore'][BUSY], self._colorscheme['back'][BUSY])) QTimer.singleShot(1000, self._applystatuscolor) else: self._applystatuscolor()
def propertyUpdated(self, pname, value): NicosWidget.propertyUpdated(self, pname, value) if pname == 'filepath': self._filePath = findResource(value) self.setPicture() elif pname == 'name': layout = QVBoxLayout() if value: layout.addWidget(self.namelabel) layout.addSpacing(5) layout.addWidget(self.piclabel, 1) sip.delete(self.layout()) self.setLayout(layout) self.namelabel.setText(value) elif pname in ('width', 'height'): self.setPicture() elif pname == 'refresh': if value: self._refreshTimer = QTimer() self._refreshTimer.setInterval(value * 1000) self._refreshTimer.timeout.connect(self.updatePicture) self._refreshTimer.start()
def addcurve(self, key, index, title, scale, offset): series = TimeSeries(key, self.props['plotinterval'], scale, offset, self.props['plotwindow'], self) series.init_empty() curve = PlotCurve([currenttime()], [0], legend=title) self.plotcurves[series] = curve self.ncurves += 1 self.curves.append(curve) self.axes.addCurves(curve) self.series[key, index] = series self.widget.update() # record the current value at least every 5 seconds, to avoid curves # not updating if the value doesn't change def update(): series.synthesize_value() self.ctimers[curve] = QTimer(singleShot=True) self.ctimers[curve].timeout.connect(update)
class EntryWidget(base_class, ui_class): def __init__(self, client, watcher, entry, shortKey, showTimeStamp, showTTL, parent=None): base_class.__init__(self, parent) self.setupUi(self) self.updateTimer = QTimer(self) self.updateTimer.setSingleShot(True) self.watcher = watcher self.client = client self.entry = entry self.widgetValue = None self.setupEvents() self.setupWidgetUi(shortKey, showTimeStamp, showTTL) def setupEvents(self): """Sets up all events.""" self.buttonSet.clicked.connect(self.setKey) self.buttonDel.clicked.connect(self.delKey) self.buttonWatch.clicked.connect(self.watchKey) self.client.signals.keyUpdated.connect(self.keyUpdated) self.updateTimer.timeout.connect(self.updateTimerEvent) def setupWidgetUi(self, shortKey, showTimeStamp, showTTL): """ Sets up and generate a UI according to the data type of the value and whether or not time to live or time stamp should be shown. """ entry = self.entry fm = self.labelTime.fontMetrics() margins = self.labelTime.getContentsMargins() self.labelTime.setMinimumWidth( fm.width(entry.convertTime(1.0)) + margins[0] + margins[2] + self.labelTime.sizeHint().width()) if self.watcher is None: # widget is already in watcher self.buttonWatch.hide() if shortKey: self.labelKey.setText(entry.key.rpartition('/')[2]) self.labelKey.setToolTip(entry.key) else: self.labelKey.setText(entry.key) if entry.value in ('True', 'False'): self.widgetValue = ReadOnlyCheckBox() self.layoutWidget.insertWidget(4, self.widgetValue) self.layoutWidget.insertSpacerItem( 5, QSpacerItem(56, 20, QSizePolicy.Expanding)) else: self.widgetValue = QLineEdit() self.layoutWidget.insertWidget(4, self.widgetValue) self.widgetValue.setReadOnly(True) self.widgetValue.setToolTip(entry.key) if not showTTL: self.labelTTL.hide() if not showTimeStamp: self.labelTime.hide() self.updateValues() def updateValues(self): entry = self.entry if entry.expired: setBackgroundColor(self, expiredColor) elif entry.ttl: setBackgroundColor(self, ttlColor) if isinstance(self.widgetValue, ReadOnlyCheckBox): self.widgetValue.setChecked(entry.value == 'True') else: self.widgetValue.setText(entry.value) self.labelTTL.setText(str(entry.ttl or '')) self.labelTime.setText(entry.convertTime()) if entry.ttl: # automatically refresh the value if the entry has a ttl (we don't # get timestamp updates from the server unless the value changes) time_to_update = max((entry.time + entry.ttl) - time.time(), 0) self.updateTimer.start(time_to_update * 1000) def setKey(self): """Sets the key locally and on the server.""" dlg = EntryEditDialog(self) dlg.fillEntry(self.entry) dlg.valueTime.setText('') # we want current timestamp by default dlg.valueKey.setReadOnly(True) if dlg.exec_() != QDialog.Accepted: return entry = dlg.getEntry() self.client.put(entry.key, entry) def delKey(self): if QMessageBox.question(self, 'Delete', 'Really delete?', QMessageBox.Yes | QMessageBox.No) == QMessageBox.No: return self.client.delete(self.entry.key) def watchKey(self): """Adds our key to the watcher window.""" if not self.watcher: return widget = EntryWidget(self.client, None, self.entry, False, True, True, self.watcher) self.watcher.addWidgetKey(widget) self.watcher.show() def keyUpdated(self, key, entry): if key != self.entry.key: return self.entry = entry self.updateValues() def updateTimerEvent(self): self.client.update(self.entry.key)
class PictureDisplay(NicosWidget, QWidget): """A display widget to show a picture.""" designer_description = 'Widget to display a picture file' filepath = PropDef('filepath', str, '', 'Path to the picture that should ' 'be displayed') name = PropDef('name', str, '', 'Name (caption) to be displayed above ' 'the picture') refresh = PropDef('refresh', int, 0, 'Interval to check for updates ' 'in seconds') height = PropDef('height', int, 0) width = PropDef('width', int, 0) def __init__(self, parent=None, designMode=False, **kwds): QWidget.__init__(self, parent, **kwds) NicosWidget.__init__(self) self._last_mtime = None self.namelabel = QLabel(self) self.namelabel.setAlignment(Qt.AlignHCenter) self.piclabel = QLabel(self) self.piclabel.setScaledContents(True) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.piclabel, 1) self.setLayout(layout) def registerKeys(self): pass def setPicture(self): size = QSize(self.props['width'] * self._scale, self.props['height'] * self._scale) if isfile(self._filePath): pixmap = QPixmap(self._filePath) else: pixmap = QPixmap(size) pixmap.fill() if size.isEmpty(): self.piclabel.setPixmap(pixmap) else: self.piclabel.setPixmap(pixmap.scaled(size)) self.piclabel.resize(self.piclabel.sizeHint()) def updatePicture(self): if not isfile(self._filePath): return # on first iteration self._last_mtime is None -> always setPicture() mtime = getmtime(self._filePath) if self._last_mtime != mtime: self._last_mtime = mtime self.setPicture() def propertyUpdated(self, pname, value): NicosWidget.propertyUpdated(self, pname, value) if pname == 'filepath': self._filePath = findResource(value) self.setPicture() elif pname == 'name': layout = QVBoxLayout() if value: layout.addWidget(self.namelabel) layout.addSpacing(5) layout.addWidget(self.piclabel, 1) sip.delete(self.layout()) self.setLayout(layout) self.namelabel.setText(value) elif pname in ('width', 'height'): self.setPicture() elif pname == 'refresh': if value: self._refreshTimer = QTimer() self._refreshTimer.setInterval(value * 1000) self._refreshTimer.timeout.connect(self.updatePicture) self._refreshTimer.start()
class MainWindow(DlgUtils, QMainWindow): name = 'MainWindow' # Emitted when a panel generates code that an editor panel should add. codeGenerated = pyqtSignal(object) # Interval (in ms) to make "keepalive" queries to the daemon. keepaliveInterval = 12 * 3600 * 1000 ui = 'main.ui' def __init__(self, log, gui_conf, viewonly=False, tunnel=''): QMainWindow.__init__(self) DlgUtils.__init__(self, 'NICOS') loadUi(self, self.ui) # set app icon in multiple sizes icon = QIcon() icon.addFile(':/appicon') icon.addFile(':/appicon-16') icon.addFile(':/appicon-48') self.setWindowIcon(icon) if tunnel and SSHTunnelForwarder is None: self.showError('You want to establish a connection to NICOS via ' "a SSH tunnel, but the 'sshtunnel' module is not " 'installed. The tunneling feature will disabled.') self.tunnel = tunnel if SSHTunnelForwarder is not None else '' self.tunnelServer = None # hide admin label until we are connected as admin self.adminLabel.hide() # our logger instance self.log = log # window for displaying errors self.errorWindow = None # window for "prompt" event confirmation self.promptWindow = None # debug console window, if opened self.debugConsole = None # log messages sent by the server self.messages = [] # are we in expert mode? (always false on startup) self.expertmode = False # no wrapping at startup self.allowoutputlinewrap = False # set-up the initial connection data self.conndata = ConnectionData(host='localhost', port=1301, user='******', password=None, viewonly=viewonly) # state members self.current_status = None # connect the client's events self.client = NicosGuiClient(self, self.log) self.client.error.connect(self.on_client_error) self.client.broken.connect(self.on_client_broken) self.client.failed.connect(self.on_client_failed) self.client.connected.connect(self.on_client_connected) self.client.disconnected.connect(self.on_client_disconnected) self.client.status.connect(self.on_client_status) self.client.showhelp.connect(self.on_client_showhelp) self.client.clientexec.connect(self.on_client_clientexec) self.client.plugplay.connect(self.on_client_plugplay) self.client.watchdog.connect(self.on_client_watchdog) self.client.prompt.connect(self.on_client_prompt) # data handling setup self.data = DataHandler(self.client) # panel configuration self.gui_conf = gui_conf self.initDataReaders() self.mainwindow = self # determine if there is an editor window type, because we would like to # have a way to open files from a console panel later self.editor_wintype = self.gui_conf.find_panel( ('editor.EditorPanel', 'nicos.clients.gui.panels.editor.EditorPanel')) self.history_wintype = self.gui_conf.find_panel( ('history.HistoryPanel', 'nicos.clients.gui.panels.history.HistoryPanel')) # additional panels self.panels = [] self.splitters = [] self.windowtypes = [] self.windows = {} # setting presets self.instrument = self.gui_conf.name self.createWindowContent() # timer for reconnecting self.reconnectTimer = QTimer(singleShot=True, timeout=self._reconnect) self._reconnect_count = 0 self._reconnect_time = 0 # timer for session keepalive, every 12 hours self.keepaliveTimer = QTimer(singleShot=False, timeout=self._keepalive) self.keepaliveTimer.start(self.keepaliveInterval) # setup tray icon self.trayIcon = QSystemTrayIcon(self) self.trayIcon.activated.connect(self.on_trayIcon_activated) self.trayMenu = QMenu(self) nameAction = self.trayMenu.addAction(self.instrument) nameAction.setEnabled(False) self.trayMenu.addSeparator() toggleAction = self.trayMenu.addAction('Hide main window') toggleAction.setCheckable(True) toggleAction.triggered[bool].connect( lambda hide: self.setVisible(not hide)) self.trayIcon.setContextMenu(self.trayMenu) # help window self.helpWindow = None # watchdog window self.watchdogWindow = None # plug-n-play notification windows self.pnpWindows = {} # create initial state self._init_toolbar() def _init_toolbar(self): self.statusLabel = QLabel('', self, pixmap=QPixmap(':/disconnected'), margin=5, minimumSize=QSize(30, 10)) self.toolbar = self.toolBarMain self.toolbar.addWidget(self.statusLabel) self.setStatus('disconnected') def addPanel(self, panel, always=True): if always or panel not in self.panels: self.panels.append(panel) def createWindowContent(self): self.sgroup = SettingGroup('MainWindow') with self.sgroup as settings: loadUserStyle(self, settings) # load saved settings and stored layout for panel config self.loadSettings(settings) # create panels in the main window widget = createWindowItem(self.gui_conf.main_window, self, self, self, self.log) if widget: self.centralLayout.addWidget(widget) self.centralLayout.setContentsMargins(0, 0, 0, 0) # call postInit after creation of all panels for panel in self.panels: panel.postInit() with self.sgroup as settings: # geometry and window appearance loadBasicWindowSettings(self, settings) self.update() # load auxiliary windows state self.loadAuxWindows(settings) if len(self.splitstate) == len(self.splitters): for sp, st in zip(self.splitters, self.splitstate): sp.restoreState(st) if not self.gui_conf.windows: self.menuBar().removeAction(self.menuWindows.menuAction()) for i, wconfig in enumerate(self.gui_conf.windows): action = ToolAction(self.client, QIcon(':/' + wconfig.icon), wconfig.name, wconfig.options, self) self.toolBarWindows.addAction(action) self.menuWindows.addAction(action) def window_callback(on, i=i): self.createWindow(i) action.triggered[bool].connect(window_callback) if not self.gui_conf.windows: self.toolBarWindows.hide() else: self.toolBarWindows.show() createToolMenu(self, self.gui_conf.tools, self.menuTools) if isinstance(self.gui_conf.main_window, tabbed) and widget: widget.tabChangedTab(0) def createWindow(self, wtype): # for the history_wintype or editor_wintype if wtype == -1: return self try: wconfig = self.gui_conf.windows[wtype] except IndexError: # config outdated, window type doesn't exist return if wtype in self.windows: window = self.windows[wtype] window.activateWindow() return window window = AuxiliaryWindow(self, wtype, wconfig) if window.centralLayout.count(): window.setWindowIcon(QIcon(':/' + wconfig.icon)) self.windows[wtype] = window window.closed.connect(self.on_auxWindow_closed) for panel in window.panels: panel.updateStatus(self.current_status) window.show() return window else: del window return None def getPanel(self, panelName): for panelobj in self.panels: if panelobj.panelName == panelName: return panelobj def initDataReaders(self): try: # just import to register all default readers # pylint: disable=unused-import import nicos.devices.datasinks except ImportError: pass classes = self.gui_conf.options.get('reader_classes', []) for clsname in classes: try: importString(clsname) except ImportError: pass def on_auxWindow_closed(self, window): del self.windows[window.type] window.deleteLater() def setConnData(self, data): self.conndata = data def _reconnect(self): if self._reconnect_count and self.conndata.password is not None: self._reconnect_count -= 1 if self._reconnect_count <= self.client.RECONNECT_TRIES_LONG: self._reconnect_time = self.client.RECONNECT_INTERVAL_LONG self.client.connect(self.conndata) def _keepalive(self): if self.client.isconnected: self.client.ask('keepalive') def show(self): QMainWindow.show(self) if self.autoconnect and not self.client.isconnected: self.on_actionConnect_triggered(True) if sys.platform == 'darwin': # on Mac OS loadBasicWindowSettings seems not to work before show() # so we do it here again with self.sgroup as settings: loadBasicWindowSettings(self, settings) def startup(self): self.show() startStartupTools(self, self.gui_conf.tools) def loadSettings(self, settings): self.autoconnect = settings.value('autoconnect', True, bool) self.connpresets = {} # new setting key, with dictionary values for (k, v) in settings.value('connpresets_new', {}).items(): self.connpresets[k] = ConnectionData(**v) # if it was empty, try old setting key with list values if not self.connpresets: for (k, v) in settings.value('connpresets', {}).items(): self.connpresets[k] = ConnectionData(host=v[0], port=int(v[1]), user=v[2], password=None) self.lastpreset = settings.value('lastpreset', '') if self.lastpreset in self.connpresets: self.conndata = self.connpresets[self.lastpreset].copy() self.instrument = settings.value('instrument', self.gui_conf.name) self.confirmexit = settings.value('confirmexit', True, bool) self.warnwhenadmin = settings.value('warnwhenadmin', True, bool) self.showtrayicon = settings.value('showtrayicon', True, bool) self.autoreconnect = settings.value('autoreconnect', True, bool) self.autosavelayout = settings.value('autosavelayout', True, bool) self.allowoutputlinewrap = settings.value('allowoutputlinewrap', False, bool) self.update() def loadAuxWindows(self, settings): open_wintypes = settings.value('auxwindows') or [] if isinstance(open_wintypes, str): open_wintypes = [int(w) for w in open_wintypes.split(',')] for wtype in open_wintypes: if isinstance(wtype, str): wtype = int(wtype) self.createWindow(wtype) def saveWindowLayout(self): with self.sgroup as settings: settings.setValue('geometry', self.saveGeometry()) settings.setValue('windowstate', self.saveState()) settings.setValue('splitstate', [sp.saveState() for sp in self.splitters]) open_wintypes = list(self.windows) settings.setValue('auxwindows', open_wintypes) def saveSettings(self, settings): settings.setValue('autoconnect', self.client.isconnected) settings.setValue( 'connpresets_new', {k: v.serialize() for (k, v) in self.connpresets.items()}) settings.setValue('lastpreset', self.lastpreset) settings.setValue('font', self.user_font) settings.setValue('color', self.user_color) def closeEvent(self, event): if self.confirmexit and QMessageBox.question( self, 'Quit', 'Do you really want to quit?', QMessageBox.Yes | QMessageBox.No) == QMessageBox.No: event.ignore() return for panel in self.panels: if not panel.requestClose(): event.ignore() return if self.autosavelayout: self.saveWindowLayout() with self.sgroup as settings: self.saveSettings(settings) for panel in self.panels: with panel.sgroup as settings: panel.saveSettings(settings) for window in list(self.windows.values()): if not window.close(): event.ignore() return if self.helpWindow: self.helpWindow.close() if self.client.isconnected: self.on_actionConnect_triggered(False) event.accept() QApplication.instance().quit() def setTitlebar(self, connected): inststr = str(self.instrument) or 'NICOS' if connected: hoststr = '%s at %s:%s' % (self.client.login, self.client.host, self.client.port) self.setWindowTitle('%s - %s' % (inststr, hoststr)) else: self.setWindowTitle('%s - disconnected' % inststr) def setStatus(self, status, exception=False): if status == self.current_status: return if self.client.last_action_at and \ self.current_status == 'running' and \ status in ('idle', 'paused') and \ currenttime() - self.client.last_action_at > 20: # show a visual indication of what happened if status == 'paused': msg = 'Script is now paused.' elif exception: msg = 'Script has exited with an error.' else: msg = 'Script has finished.' self.trayIcon.showMessage(self.instrument, msg) self.client.last_action_at = 0 self.current_status = status isconnected = status != 'disconnected' self.actionConnect.setChecked(isconnected) if isconnected: self.actionConnect.setText('Disconnect') else: self.actionConnect.setText('Connect to server...') self.setTitlebar(False) # new status icon pixmap = QPixmap(':/' + status + ('exc' if exception else '')) self.statusLabel.setPixmap(pixmap) self.statusLabel.setToolTip('Script status: %s' % status) newicon = QIcon() newicon.addPixmap(pixmap, QIcon.Disabled) self.trayIcon.setIcon(newicon) self.trayIcon.setToolTip('%s status: %s' % (self.instrument, status)) if self.showtrayicon: self.trayIcon.show() if self.promptWindow and status != 'paused': self.promptWindow.close() # propagate to panels for panel in self.panels: panel.updateStatus(status, exception) for window in self.windows.values(): for panel in window.panels: panel.updateStatus(status, exception) def on_client_error(self, problem, exc=None): if exc is not None: self.log.error('Error from daemon', exc=exc) problem = strftime('[%m-%d %H:%M:%S] ') + problem if self.errorWindow is None: def reset_errorWindow(): self.errorWindow = None self.errorWindow = ErrorDialog(self, windowTitle='Daemon error') self.errorWindow.accepted.connect(reset_errorWindow) self.errorWindow.addMessage(problem) self.errorWindow.show() else: self.errorWindow.addMessage(problem) def on_client_broken(self, problem): self.on_client_error(problem) if self.autoreconnect: self._reconnect_count = self.client.RECONNECT_TRIES self._reconnect_time = self.client.RECONNECT_INTERVAL_SHORT self.reconnectTimer.start(self._reconnect_time) def on_client_failed(self, problem): if self._reconnect_count: self.reconnectTimer.start(self._reconnect_time) else: self.on_client_error(problem) def on_client_connected(self): self.setStatus('idle') self._reconnect_count = 0 self.setTitlebar(True) # get all server status info initstatus = self.client.ask('getstatus') if initstatus: # handle initial status self.on_client_status(initstatus['status']) # propagate info to all components self.client.signal('initstatus', initstatus) # show warning label for admin users self.adminLabel.setVisible(self.warnwhenadmin and self.client.user_level is not None and self.client.user_level >= ADMIN) self.actionViewOnly.setChecked(self.client.viewonly) # set focus to command input, if present for panel in self.panels: if isinstance(panel, ConsolePanel) and panel.hasinput: panel.commandInput.setFocus() def on_client_status(self, data): status = data[0] if status == STATUS_IDLE: self.setStatus('idle') elif status == STATUS_IDLEEXC: self.setStatus('idle', exception=True) elif status != STATUS_INBREAK: self.setStatus('running') else: self.setStatus('paused') def on_client_disconnected(self): self.adminLabel.setVisible(False) self.setStatus('disconnected') def on_client_showhelp(self, data): if not HelpWindow: return if self.helpWindow is None: self.helpWindow = HelpWindow(self, self.client) self.helpWindow.showHelp(data) self.helpWindow.activateWindow() def on_client_clientexec(self, data): # currently used for client-side plot using matplotlib; data is # (funcname, args, ...) plot_func_path = data[0] try: modname, funcname = plot_func_path.rsplit('.', 1) func = getattr(__import__(modname, None, None, [funcname]), funcname) func(*data[1:]) except Exception: self.log.exception( 'Error during clientexec:\n%s', '\n'.join(traceback.format_tb(sys.exc_info()[2]))) def on_client_plugplay(self, data): windowkey = data[0:2] # (mode, setupname) if windowkey in self.pnpWindows: self.pnpWindows[windowkey].activateWindow() else: window = PnPSetupQuestion(self, self.client, data) self.pnpWindows[windowkey] = window window.closed.connect(self.on_pnpWindow_closed) window.show() def on_pnpWindow_closed(self, window): self.pnpWindows.pop(window.data[0:2], None) def on_client_watchdog(self, data): if self.watchdogWindow is None: self.watchdogWindow = WatchdogDialog(self) self.watchdogWindow.addEvent(data) if data[0] != 'resolved': self.watchdogWindow.show() def on_client_prompt(self, data): if self.promptWindow: self.promptWindow.close() # show non-modal dialog box that prompts the user to continue or abort prompt_text = data[0] dlg = self.promptWindow = QMessageBox( QMessageBox.Information, 'Confirmation required', prompt_text, QMessageBox.Ok | QMessageBox.Cancel, self) dlg.setWindowModality(Qt.NonModal) # give the buttons better descriptions btn = dlg.button(QMessageBox.Cancel) btn.setText('Abort script') btn.clicked.connect(lambda: self.client.tell_action('stop', BREAK_NOW)) btn = dlg.button(QMessageBox.Ok) btn.setText('Continue script') btn.clicked.connect(lambda: self.client.tell_action('continue')) btn.setFocus() dlg.show() def on_trayIcon_activated(self, reason): if reason == QSystemTrayIcon.Trigger: self.activateWindow() def on_actionExpert_toggled(self, on): self.expertmode = on for panel in self.panels: panel.setExpertMode(on) for window in self.windows.values(): for panel in window.panels: panel.setExpertMode(on) def on_actionViewOnly_toggled(self, on): # also triggered when the action is checked by on_client_connected self.client.viewonly = on for panel in self.panels: panel.setViewOnly(on) for window in self.windows.values(): for panel in window.panels: panel.setViewOnly(on) @pyqtSlot() def on_actionNicosHelp_triggered(self): if not HelpWindow: self.showError('Cannot open help window: Qt web extension is not ' 'available on your system.') return if not self.client.isconnected: self.showError('Cannot open online help: you are not connected ' 'to a daemon.') return self.client.eval('session.showHelp("index")') @pyqtSlot() def on_actionNicosDocu_triggered(self): if not QWebView: self.showError('Cannot open documentation window: Qt web extension' ' is not available on your system.') return from nicos.clients.gui.tools.website import WebsiteTool # XXX: change URL to current release version dlg = WebsiteTool(self, self.client, url='http://www.nicos-controls.org') dlg.setWindowModality(Qt.NonModal) dlg.show() @pyqtSlot() def on_actionDebugConsole_triggered(self): if self.debugConsole is None: self.debugConsole = DebugConsole(self) self.debugConsole.show() @pyqtSlot() def on_actionAbout_triggered(self): import nicos.authors if self.client.isconnected: dinfo = self.client.daemon_info.copy() dinfo['server_host'] = self.client.host else: dinfo = {} dlg = dialogFromUi(self, 'dialogs/about.ui') dlg.clientVersion.setText(nicos_version) dlg.pyVersion.setText( '%s/%s/%s' % (sys.version.split()[0], QT_VERSION_STR, PYQT_VERSION_STR)) dlg.serverHost.setText(dinfo.get('server_host', 'not connected')) dlg.nicosRoot.setText(dinfo.get('nicos_root', '')) dlg.serverVersion.setText(dinfo.get('daemon_version', '')) dlg.customPath.setText(dinfo.get('custom_path', '')) dlg.customVersion.setText(dinfo.get('custom_version', '')) dlg.contributors.setPlainText(nicos.authors.authors_list) dlg.adjustSize() dlg.exec_() @pyqtSlot(bool) def on_actionConnect_triggered(self, on): # connection or disconnection request? if not on: self.client.disconnect() if self.tunnelServer: self.tunnelServer.stop() self.tunnelServer = None return self.actionConnect.setChecked(False) # gets set by connection event ret = ConnectionDialog.getConnectionData(self, self.connpresets, self.lastpreset, self.conndata, self.tunnel) new_name, new_data, save, tunnel = ret if new_data is None: return if save: self.lastpreset = save self.connpresets[save] = new_data else: self.lastpreset = new_name self.conndata = new_data if tunnel: try: host, username, password = splitTunnelString(tunnel) self.tunnelServer = SSHTunnelForwarder( host, ssh_username=username, ssh_password=password, remote_bind_address=(self.conndata.host, self.conndata.port), compression=True) self.tunnelServer.start() tunnel_port = self.tunnelServer.local_bind_port # corresponding ssh command line (debug) # print 'ssh -f %s -L %d:%s:%d -N' % (host, tunnel_port, # self.conndata.host, # self.conndata.port) # store the established tunnel information host, user, and # password for the next connection try to avoid typing password # for every (re)connection via the GUI self.tunnel = tunnel self.conndata.host = 'localhost' self.conndata.port = tunnel_port except ValueError as e: self.showError(str(e)) self.tunnelServer = None except BaseSSHTunnelForwarderError as e: self.showError(str(e)) self.tunnelServer = None self.client.connect(self.conndata) @pyqtSlot() def on_actionPreferences_triggered(self): dlg = SettingsDialog(self) ret = dlg.exec_() if ret == QDialog.Accepted: dlg.saveSettings() @pyqtSlot() def on_actionFont_triggered(self): font, ok = QFontDialog.getFont(self.user_font, self) if not ok: return for panel in self.panels: panel.setCustomStyle(font, self.user_color) self.user_font = font @pyqtSlot() def on_actionColor_triggered(self): color = QColorDialog.getColor(self.user_color, self) if not color.isValid(): return for panel in self.panels: panel.setCustomStyle(self.user_font, color) self.user_color = color
def __init__(self, log, gui_conf, viewonly=False, tunnel=''): QMainWindow.__init__(self) DlgUtils.__init__(self, 'NICOS') loadUi(self, self.ui) # set app icon in multiple sizes icon = QIcon() icon.addFile(':/appicon') icon.addFile(':/appicon-16') icon.addFile(':/appicon-48') self.setWindowIcon(icon) if tunnel and SSHTunnelForwarder is None: self.showError('You want to establish a connection to NICOS via ' "a SSH tunnel, but the 'sshtunnel' module is not " 'installed. The tunneling feature will disabled.') self.tunnel = tunnel if SSHTunnelForwarder is not None else '' self.tunnelServer = None # hide admin label until we are connected as admin self.adminLabel.hide() # our logger instance self.log = log # window for displaying errors self.errorWindow = None # window for "prompt" event confirmation self.promptWindow = None # debug console window, if opened self.debugConsole = None # log messages sent by the server self.messages = [] # are we in expert mode? (always false on startup) self.expertmode = False # no wrapping at startup self.allowoutputlinewrap = False # set-up the initial connection data self.conndata = ConnectionData(host='localhost', port=1301, user='******', password=None, viewonly=viewonly) # state members self.current_status = None # connect the client's events self.client = NicosGuiClient(self, self.log) self.client.error.connect(self.on_client_error) self.client.broken.connect(self.on_client_broken) self.client.failed.connect(self.on_client_failed) self.client.connected.connect(self.on_client_connected) self.client.disconnected.connect(self.on_client_disconnected) self.client.status.connect(self.on_client_status) self.client.showhelp.connect(self.on_client_showhelp) self.client.clientexec.connect(self.on_client_clientexec) self.client.plugplay.connect(self.on_client_plugplay) self.client.watchdog.connect(self.on_client_watchdog) self.client.prompt.connect(self.on_client_prompt) # data handling setup self.data = DataHandler(self.client) # panel configuration self.gui_conf = gui_conf self.initDataReaders() self.mainwindow = self # determine if there is an editor window type, because we would like to # have a way to open files from a console panel later self.editor_wintype = self.gui_conf.find_panel( ('editor.EditorPanel', 'nicos.clients.gui.panels.editor.EditorPanel')) self.history_wintype = self.gui_conf.find_panel( ('history.HistoryPanel', 'nicos.clients.gui.panels.history.HistoryPanel')) # additional panels self.panels = [] self.splitters = [] self.windowtypes = [] self.windows = {} # setting presets self.instrument = self.gui_conf.name self.createWindowContent() # timer for reconnecting self.reconnectTimer = QTimer(singleShot=True, timeout=self._reconnect) self._reconnect_count = 0 self._reconnect_time = 0 # timer for session keepalive, every 12 hours self.keepaliveTimer = QTimer(singleShot=False, timeout=self._keepalive) self.keepaliveTimer.start(self.keepaliveInterval) # setup tray icon self.trayIcon = QSystemTrayIcon(self) self.trayIcon.activated.connect(self.on_trayIcon_activated) self.trayMenu = QMenu(self) nameAction = self.trayMenu.addAction(self.instrument) nameAction.setEnabled(False) self.trayMenu.addSeparator() toggleAction = self.trayMenu.addAction('Hide main window') toggleAction.setCheckable(True) toggleAction.triggered[bool].connect( lambda hide: self.setVisible(not hide)) self.trayIcon.setContextMenu(self.trayMenu) # help window self.helpWindow = None # watchdog window self.watchdogWindow = None # plug-n-play notification windows self.pnpWindows = {} # create initial state self._init_toolbar()
class ValueDisplay(NicosWidget, QWidget): """Value display widget with two labels.""" designer_description = 'A widget with name/value labels' designer_icon = ':/table' widgetInfo = pyqtSignal(str) dev = PropDef('dev', str, '', 'NICOS device name, if set, display ' 'value of this device') key = PropDef('key', str, '', 'Cache key to display (without "nicos/"' ' prefix), set either "dev" or this') statuskey = PropDef('statuskey', str, '', 'Cache key to extract status ' 'information for coloring value, if "dev" is ' 'given this is set automatically') name = PropDef('name', str, '', 'Name of the value to display above/' 'left of the value; if "dev" is given this ' 'defaults to the device name') unit = PropDef('unit', str, '', 'Unit of the value to display next to ' 'the name; if "dev" is given this defaults to ' 'the unit set in NICOS') format = PropDef('format', str, '', 'Python format string to use for the ' 'value; if "dev" is given this defaults to the ' '"fmtstr" set in NICOS') maxlen = PropDef('maxlen', int, -1, 'Maximum length of the value string to ' 'allow; defaults to no limit') width = PropDef('width', int, 8, 'Width of the widget in units of the ' 'width of one character') istext = PropDef('istext', bool, False, 'If given, a "text" font will be ' 'used for the value instead of the monospaced ' 'font used for numeric values') showName = PropDef('showName', bool, True, 'If false, do not display the ' 'label for the value name') showStatus = PropDef('showStatus', bool, True, 'If false, do not display ' 'the device status as a color of the value text') showExpiration = PropDef('showExpiration', bool, True, 'If true, display ' 'expired cache values as "n/a"') horizontal = PropDef('horizontal', bool, False, 'If true, display name ' 'label left of the value instead of above it') def __init__(self, parent, designMode=False, colorScheme=None, **kwds): # keys being watched self._mainkeyid = None self._statuskeyid = None # other current values self._isfixed = '' # XXX could be taken from devinfo self._lastvalue = designMode and '1.4' or None self._laststatus = (OK, '') self._lastchange = 0 self._mouseover = False self._mousetimer = None self._expired = True self._colorscheme = colorScheme or defaultColorScheme QWidget.__init__(self, parent, **kwds) NicosWidget.__init__(self) self._statuscolors = self._colorscheme['fore'][UNKNOWN], \ self._colorscheme['back'][UNKNOWN] self._labelcolor = None def propertyUpdated(self, pname, value): if pname == 'dev': if value: self.key = value + '.value' self.statuskey = value + '.status' elif pname == 'width': if value < 0: self.reinitLayout() else: onechar = QFontMetrics(self.valueFont).width('0') self.valuelabel.setMinimumSize(QSize(onechar * (value + .5), 0)) elif pname == 'istext': self.valuelabel.setFont(value and self.font() or self.valueFont) self.width = self.width elif pname == 'valueFont': self.valuelabel.setFont(self.valueFont) self.width = self.width # update char width calculation elif pname == 'showName': self.namelabel.setVisible(value) elif pname == 'showStatus': if not value: setBothColors(self.valuelabel, (self._colorscheme['fore'][UNKNOWN], self._colorscheme['back'][UNKNOWN])) elif pname == 'horizontal': self.reinitLayout() if pname in ('dev', 'name', 'unit'): self.update_namelabel() NicosWidget.propertyUpdated(self, pname, value) def initUi(self): self.namelabel = QLabel(' ', self, textFormat=Qt.RichText) self.update_namelabel() valuelabel = SensitiveSMLabel('----', self, self._label_entered, self._label_left) valuelabel.setFrameShape(QFrame.Panel) valuelabel.setAlignment(Qt.AlignHCenter) valuelabel.setFrameShadow(QFrame.Sunken) valuelabel.setAutoFillBackground(True) setBothColors(valuelabel, (self._colorscheme['fore'][UNKNOWN], self._colorscheme['back'][UNKNOWN])) valuelabel.setLineWidth(2) self.valuelabel = valuelabel self.width = 8 self.reinitLayout() def reinitLayout(self): # reinitialize UI after switching horizontal/vertical layout if self.props['horizontal']: new_layout = QHBoxLayout() new_layout.addWidget(self.namelabel) new_layout.addStretch() new_layout.addWidget(self.valuelabel) self.namelabel.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) else: new_layout = QVBoxLayout() new_layout.addWidget(self.namelabel) tmplayout = QHBoxLayout() if self.width >= 0: tmplayout.addStretch() tmplayout.addWidget(self.valuelabel) if self.width >= 0: tmplayout.addStretch() new_layout.addLayout(tmplayout) self.namelabel.setAlignment(Qt.AlignHCenter) if self.layout(): sip.delete(self.layout()) new_layout.setContentsMargins(1, 1, 1, 1) # save space self.setLayout(new_layout) def registerKeys(self): if self.props['dev']: self.registerDevice(self.props['dev'], self.props['unit'], self.props['format']) else: self.registerKey(self.props['key'], self.props['statuskey'], self.props['unit'], self.props['format']) def on_devValueChange(self, dev, value, strvalue, unitvalue, expired): # check expired values self._expired = expired self._lastvalue = value self._lastchange = currenttime() if self.props['maxlen'] > -1: self.valuelabel.setText(strvalue[:self.props['maxlen']]) else: self.valuelabel.setText(strvalue) if self._expired: setBothColors(self.valuelabel, (self._colorscheme['fore'][UNKNOWN], self._colorscheme['expired'])) if self.props['showExpiration']: self.valuelabel.setText(NOT_AVAILABLE) elif not self.props['istext']: setBothColors(self.valuelabel, (self._colorscheme['fore'][BUSY], self._colorscheme['back'][BUSY])) QTimer.singleShot(1000, self._applystatuscolor) else: self._applystatuscolor() def _applystatuscolor(self): if self._expired: setBothColors(self.valuelabel, (self._colorscheme['fore'][UNKNOWN], self._colorscheme['expired'])) else: setBothColors(self.valuelabel, self._statuscolors) if self._labelcolor: self.namelabel.setAutoFillBackground(True) setBackgroundColor(self.namelabel, self._labelcolor) else: self.namelabel.setAutoFillBackground(False) def on_devStatusChange(self, dev, code, status, expired): if self.props['showStatus']: self._statuscolors = self._colorscheme['fore'][code], \ self._colorscheme['back'][code] self._labelcolor = self._colorscheme['label'][code] self._laststatus = code, status self._applystatuscolor() def on_devMetaChange(self, dev, fmtstr, unit, fixed): self._isfixed = fixed and ' (F)' self.format = fmtstr self.unit = unit or '' def update_namelabel(self): name = self.props['name'] or self.props['dev'] or self.props['key'] self.namelabel.setText( html.escape(str(name)) + ' <font color="#888888">%s</font><font color="#0000ff">%s</font> ' % (html.escape(self.props['unit'].strip()), self._isfixed)) def _label_entered(self, widget, event, from_mouse=True): infotext = '%s = %s' % (self.props['name'] or self.props['dev'] or self.props['key'], self.valuelabel.text()) if self.props['unit'].strip(): infotext += ' %s' % self.props['unit'] if self.props['statuskey']: try: const, msg = self._laststatus except ValueError: const, msg = self._laststatus, '' infotext += ', status is %s: %s' % (statuses.get(const, '?'), msg) infotext += ', changed %s ago' % ( nicedelta(currenttime() - self._lastchange)) self.widgetInfo.emit(infotext) if from_mouse: self._mousetimer = QTimer(self, timeout=lambda: self._label_entered(widget, event, False) ) self._mousetimer.start(1000) def _label_left(self, widget, event): if self._mousetimer: self._mousetimer.stop() self._mousetimer = None self.widgetInfo.emit('')
def __init__(self, frame, view): self._id2item = {} # mapping from request ID to list widget item self._frame = frame self._view = view self._timer = QTimer(singleShot=True, timeout=self._timeout)
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 ExpInfoPanel(Panel): """Provides a panel with several labels displaying basic experiment info. This is for example the experiment title, sample name, and user name. It also provides several buttons with which the user can change proposal info, sample properties, scan environment and setups. Options: * ``sample_panel`` -- what to show when the user clicks on the "Sample" button. The value must be a panel configuration, e.g. ``panel('...')`` or ``tabbed(...)``. There are several panels that are useful for this: - ``nicos.clients.gui.panels.setup_panel.GenericSamplePanel`` -- a panel that only shows a single input box for the sample name. - ``nicos.clients.gui.panels.setup_panel.TasSamplePanel`` -- a panel that also shows input boxes for triple-axis sample properties (such as lattice constants). * ``popup_proposal_after`` -- if given, the proposal dialog will be opened when the daemon has been idle for more than the specified time interval (in hours). * ``new_exp_panel`` -- class name of the panel which should be opened after a new experiment has been started from the proposal info panel. * ``finish_exp_panel`` -- class name of the panel which should be opened before an experiment is finished from the proposal info panel. """ panelName = 'Experiment Info' _viewonly = False def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/expinfo.ui') for ch in self.findChildren(NicosWidget): ch.setClient(client) client.setup.connect(self.on_client_setup) client.initstatus.connect(self.on_client_initstatus) self.detLabel.setFormatCallback( lambda value, strvalue: ', '.join(sorted(value))) self.envLabel.setFormatCallback( lambda value, strvalue: ', '.join(sorted(value))) self._sample_panel = options.get('sample_panel', GenericSamplePanel) self._new_exp_panel = options.get('new_exp_panel') self._finish_exp_panel = options.get('finish_exp_panel') self._timeout = options.get('popup_proposal_after', 0) if self._timeout: self._proposal_popup_timer = QTimer(interval=self._timeout * 3600000) self._proposal_popup_timer.setSingleShot(True) self._proposal_popup_timer.timeout.connect( self.on_proposal_popup_timer_timeout) else: self._proposal_popup_timer = None def hideTitle(self): self.titleLbl.setVisible(False) def setViewOnly(self, viewonly): self._viewonly = viewonly if not self._viewonly and self._timeout: # ask explicitly for status to restart timer if necessary self.client.ask('getstatus') def on_client_initstatus(self, initstatus): self.setupLabel.setText(', '.join(sorted(initstatus['setups'][1]))) def on_client_setup(self, data): self.setupLabel.setText(', '.join(sorted(data[1]))) def updateStatus(self, status, exception=False): if self._proposal_popup_timer: if status == 'idle': if not self._viewonly or \ (self.client.user_level is not None and self.client.user_level < ADMIN): self._proposal_popup_timer.start() else: self._proposal_popup_timer.stop() def on_proposal_popup_timer_timeout(self): if self._viewonly: return dlg = QMessageBox(self) dlg.setText('The experiment has been idle for more than %.1f hours.' % self._timeout) contButton = QPushButton('Continue current experiment') finishAndNewButton = QPushButton('Finish and start new experiment') dlg.addButton(contButton, QMessageBox.RejectRole) dlg.addButton(finishAndNewButton, QMessageBox.ActionRole) dlg.exec_() if dlg.clickedButton() == finishAndNewButton: self.on_proposalBtn_clicked() elif dlg.clickedButton() == contButton: self._proposal_popup_timer.start() @pyqtSlot() def on_proposalBtn_clicked(self): dlg = PanelDialog(self, self.client, ExpPanel, 'Proposal info', new_exp_panel=self._new_exp_panel, finish_exp_panel=self._finish_exp_panel) dlg.exec_() @pyqtSlot() def on_setupBtn_clicked(self): dlg = PanelDialog(self, self.client, SetupsPanel, 'Setups') dlg.exec_() @pyqtSlot() def on_sampleBtn_clicked(self): dlg = PanelDialog(self, self.client, self._sample_panel, 'Sample information') dlg.exec_() @pyqtSlot() def on_detenvBtn_clicked(self): dlg = PanelDialog(self, self.client, DetEnvPanel, 'Detectors and environment') dlg.exec_() @pyqtSlot() def on_remarkBtn_clicked(self): dlg = dialogFromUi(self, 'panels/expinfo_remark.ui') def callback(): self.showInfo('The remark will be added to the logbook as a ' 'heading and will also appear in the data files.') dlg.buttonBox.helpRequested.connect(callback) for ch in dlg.findChildren(NicosWidget): ch.setClient(self.client) dlg.remarkEdit.setFocus() if not dlg.exec_(): return self.client.run('Remark(%r)' % dlg.remarkEdit.getValue())
def __init__(self, widget, name, keys_indices, interval, fromtime, totime, yfrom, yto, window, meta, dlginfo, query_func): QObject.__init__(self) self.name = name self.dlginfo = dlginfo self.fromtime = fromtime self.totime = totime self.yfrom = yfrom self.yto = yto self.window = window self._key_indices = {} self.uniq_keys = set() self.series = OrderedDict() self.timer = None # + 60 seconds: get all values, also those added while querying hist_totime = self.totime or currenttime() + 60 hist_cache = {} iterator = enumerate(keys_indices) if fromtime is not None: iterator = enumerateWithProgress(keys_indices, 'Querying history...', force_display=True) for _, (key, index, scale, offset) in iterator: real_indices = [index] history = None self.uniq_keys.add(key) if fromtime is not None: if key not in hist_cache: history = query_func(key, self.fromtime, hist_totime) if not history: from nicos.clients.gui.main import log if log is None: from __main__ import log # pylint: disable=no-name-in-module log.error('Error getting history for %s.', key) QMessageBox.warning( widget, 'Error', 'Could not get history for %s, ' 'there are no values to show.\n' 'Is it spelled correctly?' % key) history = [] hist_cache[key] = history else: history = hist_cache[key] # if the value is a list/tuple and we don't have an index # specified, add a plot for each item if history: first_value = history[0][1] if not index and isinstance(first_value, (list, tuple)): real_indices = tuple( (i, ) for i in range(len(first_value))) for index in real_indices: name = '%s[%s]' % (key, ','.join(map(str, index))) if index else key series = TimeSeries(name, interval, scale, offset, window, self, meta[0].get(key), meta[1].get(key)) self.series[key, index] = series if history: series.init_from_history(history, fromtime, totime or currenttime(), index) else: series.init_empty() self._key_indices.setdefault(key, []).extend(real_indices) self.listitem = None self.plot = None if self.totime is None: # add another point with the same value every interval time (but # not more often than 11 seconds) self.timer = QTimer(self, interval=max(interval, 11) * 1000) self.timer.timeout.connect(self.on_timer_timeout) self.timer.start() self.timeSeriesUpdate.connect(self.on_timeSeriesUpdate)
class HexapodTool(DlgUtils, QMainWindow): toolName = 'HexapodTool' def __init__(self, parent, _client, **_kwds): QMainWindow.__init__(self, parent) DlgUtils.__init__(self, 'Hexapod') # set during actions that will call signal handlers self.recursive = False loadUi(self, findResource('nicos_mlz/kws1/gui/tools/hexapod.ui')) for but in (self.butStart, self.butSetWorkspace, self.butSetFrame, self.butSaveVel): but.setEnabled(False) self.axes = {} self.axeslist = [] try: self._controller = PyTango.DeviceProxy(TANGO_DEV_BASE + 'controller') # make sure the server is running and create remaining proxies try: self._controller.State() except AttributeError: raise Exception('server appears to be not running') for axis in AXES: self.axes[axis] = PyTango.DeviceProxy(TANGO_DEV_BASE + axis) self.axeslist.append(self.axes[axis]) except Exception as err: self.showError('could not connect to tango server: %s' % err) self.deleteLater() return self.on_cbsWorkspace_activated(0) self.on_cbsFrame_activated(self.cbsFrame.currentText()) tx_speed = self.query_attr(self.axes['tx'], 'speed') self.inpVelTrans.setValue(tx_speed) self.lblVelTrans.setText(self.inpVelTrans.text()) self.inpVelRot.setValue(self.query_attr(self.axes['rx'], 'speed')) self.lblVelRot.setText(self.inpVelRot.text()) self.inpVelOmega.setValue(self.query_attr(self.axes['omega'], 'speed')) self.lblVelOmega.setText(self.inpVelOmega.text()) # ramp time = speed / acceleration self.inpRampUp.setValue(tx_speed / self.query_attr(self.axes['tx'], 'accel')) self.lblRampUp.setText(self.inpRampUp.text()) self.inpRampDown.setValue(tx_speed / self.query_attr(self.axes['tx'], 'decel')) self.lblRampDown.setText(self.inpRampDown.text()) self.updTimer = QTimer() self.updTimer.timeout.connect(self.updateTimer) self.updTimer.start(1000) def exec_cmd(self, dev, cmd, args=None): try: return dev.command_inout(cmd, args) except Exception as err: self.showError('could not execute %s on hexapod:\n%s' % (cmd, err)) raise def query_attr(self, dev, attr): try: return getattr(dev, attr) except Exception as err: self.showError('could not query %s on hexapod:\n%s' % (attr, err)) raise def set_attr(self, dev, attr, value): try: setattr(dev, attr, value) except Exception as err: self.showError('could not set %s on hexapod:\n%s' % (attr, err)) raise @pyqtSlot() def on_butExit_clicked(self): self.close() self.deleteLater() def on_inpNewXX_valueChanged(self, v): self.butStart.setEnabled(True) @pyqtSlot() def on_butStart_clicked(self): self.exec_cmd( self._controller, 'StartSynchronousMovement', [ self.inpNewTX.value(), self.inpNewTY.value(), self.inpNewTZ.value(), self.inpNewRZ.value(), self.inpNewRY.value(), self.inpNewRX.value(), self.inpNewOmega.value(), 0.0, # detector arm dummy value ]) self.butStart.setEnabled(False) @pyqtSlot() def on_butStop_clicked(self): try: self.exec_cmd(self._controller, 'Stop') except Exception as err: self.showInfo('exception raised after executing stop on hexapod:\n' '%s' % err) def updateTimer(self): msg = '<font color=darkblue>' + self.exec_cmd(self._controller, 'Status') if 'not referenced' in self.exec_cmd(self.axes['omega'], 'Status'): msg += ' (omega not referenced)' msg += '</font>' self.lblStatus.setText(msg) pos = [self.query_attr(axis, 'value') for axis in self.axeslist] for value, widget in zip(pos, [ self.lblCurTX, self.lblCurTY, self.lblCurTZ, self.lblCurRZ, self.lblCurRY, self.lblCurRX, self.lblCurOmega ]): widget.setText('%8.3f' % value) @pyqtSlot(str) def on_cbsFrame_activated(self, text): frame = text.split()[0].upper() values = self.query_attr(self._controller, 'frame' + frame) for value, widget in zip(values, [ self.inpFrameTX, self.inpFrameTY, self.inpFrameTZ, self.inpFrameRZ, self.inpFrameRY, self.inpFrameRX ]): widget.setValue(value) self.butSetFrame.setEnabled(False) def on_inpFrameXX_valueChanged(self, _value): if self.cbsFrame.currentIndex() <= 3: self.butSetFrame.setEnabled(True) def on_inpWsXX_valueChanged(self, _value): if not self.recursive: self.butSetWorkspace.setEnabled(True) @pyqtSlot() def on_butSetWorkspace_clicked(self): self.recursive = True try: index = self.cbsWorkspace.currentIndex() group, index = ('juelich', index - 1) if index else ('hexamove', 0) workspaces = self.query_attr(self._controller, group + 'workspaceDefinitions') workspaces = [[i] + (ws if i != index else [ self.inpWsTXmin.value(), self.inpWsTXmax.value(), self.inpWsTYmin.value(), self.inpWsTYmax.value(), self.inpWsTZmin.value(), self.inpWsTZmax.value(), self.inpWsRZmin.value(), self.inpWsRZmax.value(), self.inpWsRYmin.value(), self.inpWsRYmax.value(), self.inpWsRXmin.value(), self.inpWsRXmax.value(), self.inpWsTXref.value(), self.inpWsTYref.value(), self.inpWsTZref.value(), self.inpWsRZref.value(), self.inpWsRYref.value(), self.inpWsRXref.value(), ]) for i, ws in enumerate(workspaces)] self._controller.set_property( {'init{}workspaces'.format(group): workspaces}) finally: self.recursive = False def on_togEnableWorkspace_toggled(self, checked): if self.recursive: return index = self.cbsWorkspace.currentIndex() group, index = ('juelich', index - 1) if index else ('hexamove', 0) self.exec_cmd( self._controller, ('Enable' if checked else 'Disable') + group + 'Workspace', index, ) @pyqtSlot(int) def on_cbsWorkspace_activated(self, index): group, index = ('juelich', index - 1) if index else ('hexamove', 0) workspace = enabled = [] for _ in range(5): workspace = self.query_attr(self._controller, group + 'WorkspaceDefinitions')[index] enabled = self.query_attr(self._controller, group + 'WorkspaceStatus')[index] # check reference frame values; if they are out of range then # do a few retries (the library sometimes returns dummy values) # TODO: check if this is still necessary with the new protocol reftx, refty, reftz, refrz, refry, refrx = workspace[-6:] if (-50 <= reftx <= +50) and (-50 <= refty <= +50) and \ (-50 <= reftz <= 510) and (-5 <= refrx <= +5) and \ (-5 <= refry <= +5) and (-180 <= refrz <= +180): break self.recursive = True try: for value, widget in zip(workspace, [ self.inpWsTXmin, self.inpWsTXmax, self.inpWsTYmin, self.inpWsTYmax, self.inpWsTZmin, self.inpWsTZmax, self.inpWsRZmin, self.inpWsRZmax, self.inpWsRYmin, self.inpWsRYmax, self.inpWsRXmin, self.inpWsRXmax, self.inpWsTXref, self.inpWsTYref, self.inpWsTZref, self.inpWsRZref, self.inpWsRYref, self.inpWsRXref, ]): widget.setValue(value) self.togEnableWorkspace.setChecked(enabled) finally: self.recursive = False self.butSetWorkspace.setEnabled(False) def on_togBirne_toggled(self, checked): self.set_attr(self._controller, 'manualControl', checked) self.grpFrames.setEnabled(not checked) self.grpNewPos.setEnabled(not checked) self.grpWorkspaces.setEnabled(not checked) @pyqtSlot() def on_butResetSystem_clicked(self): self.exec_cmd(self._controller, 'Reset') @pyqtSlot() def on_butCopyCurrent_clicked(self): for inwidget, labelwidget in zip([ self.inpNewTX, self.inpNewTY, self.inpNewTZ, self.inpNewRZ, self.inpNewRY, self.inpNewRX, self.inpNewOmega ], [ self.lblCurTX, self.lblCurTY, self.lblCurTZ, self.lblCurRZ, self.lblCurRY, self.lblCurRX, self.lblCurOmega ]): inwidget.setValue(float(labelwidget.text().replace(',', '.'))) @pyqtSlot() def on_actionReference_drive_triggered(self): self.exec_cmd(self.axes['omega'], 'Reference') @pyqtSlot() def on_actionSet_Zero_triggered(self): self.exec_cmd(self.axes['omega'], 'Adjust', 0) def on_inpVelXX_valueChanged(self, _v): self.butSaveVel.setEnabled(True) @pyqtSlot() def on_butSaveVel_clicked(self): self.set_attr(self.axes['tx'], 'speed', self.inpVelTrans.value()) self.set_attr(self.axes['rx'], 'speed', self.inpVelRot.value()) self.set_attr(self.axes['omega'], 'speed', self.inpVelOmega.value()) # acceleration = speed / ramp time self.set_attr(self.axes['tx'], 'accel', self.inpVelTrans.value() / self.inpRampUp.value()) self.set_attr(self.axes['tx'], 'decel', self.inpVelTrans.value() / self.inpRampDown.value()) self.butSaveVel.setEnabled(False) self.lblVelTrans.setText(self.inpVelTrans.text()) self.lblVelRot.setText(self.inpVelRot.text()) self.lblVelOmega.setText(self.inpVelOmega.text()) self.lblRampUp.setText(self.inpRampUp.text()) self.lblRampDown.setText(self.inpRampDown.text()) def on_inpFrameTX_valueChanged(self, v): self.on_inpFrameXX_valueChanged(v) def on_inpFrameTY_valueChanged(self, v): self.on_inpFrameXX_valueChanged(v) def on_inpFrameTZ_valueChanged(self, v): self.on_inpFrameXX_valueChanged(v) def on_inpFrameRX_valueChanged(self, v): self.on_inpFrameXX_valueChanged(v) def on_inpFrameRY_valueChanged(self, v): self.on_inpFrameXX_valueChanged(v) def on_inpFrameRZ_valueChanged(self, v): self.on_inpFrameXX_valueChanged(v) def on_inpWsTXmax_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTXmin_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTXref_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTYmax_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTYmin_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTYref_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTZmax_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTZmin_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsTZref_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRXmax_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRXmin_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRXref_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRYmax_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRYmin_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRYref_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRZmax_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRZmin_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpWsRZref_valueChanged(self, v): self.on_inpWsXX_valueChanged(v) def on_inpNewOmega_valueChanged(self, v): self.on_inpNewXX_valueChanged(v) def on_inpNewRZ_valueChanged(self, v): self.on_inpNewXX_valueChanged(v) def on_inpNewRY_valueChanged(self, v): self.on_inpNewXX_valueChanged(v) def on_inpNewRX_valueChanged(self, v): self.on_inpNewXX_valueChanged(v) def on_inpNewTZ_valueChanged(self, v): self.on_inpNewXX_valueChanged(v) def on_inpNewTY_valueChanged(self, v): self.on_inpNewXX_valueChanged(v) def on_inpNewTX_valueChanged(self, v): self.on_inpNewXX_valueChanged(v) def on_inpRampDown_valueChanged(self, v): self.on_inpVelXX_valueChanged(v) def on_inpRampUp_valueChanged(self, v): self.on_inpVelXX_valueChanged(v) def on_inpVelTrans_valueChanged(self, v): self.on_inpVelXX_valueChanged(v) def on_inpVelOmega_valueChanged(self, v): self.on_inpVelXX_valueChanged(v) def on_inpVelRot_valueChanged(self, v): self.on_inpVelXX_valueChanged(v)