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_message(self, message): # show warnings and errors emitted by the current command in a window if message[5] != self._exec_reqid or message[2] < WARNING: return msg = '%s: %s' % (message[0], message[3].strip()) if self._error_window is None: def reset_errorwindow(): self._error_window = None self._error_window = ErrorDialog(self) self._error_window.accepted.connect(reset_errorwindow) self._error_window.addMessage(msg) self._error_window.show() else: self._error_window.addMessage(msg) self._error_window.activateWindow() self._reinit()
class GenericPanel(Panel): """Provides a generic implementation of Panel to display any ``.ui`` file. The ``.ui`` file may also use NICOS GUI widgets (see :ref:`gui-widgets`). Options: * ``uifile`` -- the path to the UI file to display * ``showmsg`` -- if set to `True` a dialog window pops up in case of an error or warning inside the daemon and displays the corresponding message. If the dialog is already open, a new line with the error or warning message will be added to the open dialog. """ panelName = 'Generic' # XXX this is not unique def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) self._error_window = None if 'uifile' not in options: raise ConfigurationError('GenericPanels require at least an' ' `uifile` option.') loadUi(self, findResource(options['uifile'])) if options.get('showmsg'): self.client.message.connect(self.on_client_message) if client.isconnected: self.on_client_connected() client.connected.connect(self.on_client_connected) def on_client_connected(self): for ch in self.findChildren(NicosWidget): ch.setClient(self.client) def on_client_message(self, message): # show warnings and errors emitted by the current command in a window if len(message) < 6 or message[5] != self.client.last_reqid or \ message[2] < WARNING: return msg = '%s: %s' % (message[0], message[3].strip()) if self._error_window is None: def reset_errorwindow(): self._error_window = None self._error_window = ErrorDialog(self) self._error_window.accepted.connect(reset_errorwindow) self._error_window.addMessage(msg) self._error_window.show() else: self._error_window.addMessage(msg) self._error_window.activateWindow()
class DevicesPanel(Panel): """Provides a graphical list of NICOS devices and their current values. The user can operate basic device functions (move, stop, reset) by selecting an item from the list, which opens a control dialog. Options: * ``useicons`` (default True) -- if set to False, the list widget does not display status icons for the devices. * ``param_display`` (default {}) -- a dictionary containing the device name as key and a parameter name or a list of the parameter names which should be displayed in the device tree as subitems of the device item, for example:: param_display = { 'tas': 'scanmode', 'Exp': ['lastpoint', 'lastscan'] } * ``filters`` (default []) -- a list of tuples containing the name of the filter and the regular expression to filter out the devices. example:: filters = [ ('All', ''), ('Default', 'T|UBahn'), ('Foo', 'bar$'), ] """ panelName = 'Devices' ui = 'panels/devices.ui' @classmethod def _createIcons(cls): # hack to make non-Qt usage as in checksetups work if not hasattr(cls, 'statusIcon'): cls.statusIcon = { OK: QIcon(':/leds/status_green'), WARN: QIcon(':/leds/status_warn'), BUSY: QIcon(':/leds/status_yellow'), NOTREACHED: QIcon(':/leds/status_red'), DISABLED: QIcon(':/leds/status_white'), ERROR: QIcon(':/leds/status_red'), UNKNOWN: QIcon(':/leds/status_unknown'), } @property def groupIcon(self): return QIcon(':/setup') def __init__(self, parent, client, options): DevicesPanel._createIcons() Panel.__init__(self, parent, client, options) loadUi(self, self.ui) self.useicons = bool(options.get('icons', True)) self.param_display = {} param_display = options.get('param_display', {}) for (key, value) in param_display.items(): value = [value] if isinstance(value, string_types) else list(value) self.param_display[key.lower()] = value self.tree.header().restoreState(self._headerstate) self.clear() self.devmenu = QMenu(self) self.devmenu.addAction(self.actionMove) self.devmenu.addAction(self.actionStop) self.devmenu.addAction(self.actionReset) self.devmenu.addSeparator() self.devmenu.addAction(self.actionFix) self.devmenu.addAction(self.actionRelease) self.devmenu.addSeparator() if self.mainwindow.history_wintype is not None: self.devmenu.addAction(self.actionPlotHistory) self.devmenu.addSeparator() self.devmenu.addAction(self.actionShutDown) self.devmenu.addAction(self.actionHelp) self.devmenu_ro = QMenu(self) self.devmenu_ro.addAction(self.actionMove) self.devmenu_ro.addAction(self.actionReset) self.devmenu_ro.addSeparator() if self.mainwindow.history_wintype is not None: self.devmenu_ro.addAction(self.actionPlotHistory) self.devmenu_ro.addSeparator() self.devmenu_ro.addAction(self.actionShutDown) self.devmenu_ro.addAction(self.actionHelp) self._menu_dev = None # device for which context menu is shown self._dev2setup = {} self._setupinfo = {} self._control_dialogs = {} self._show_lowlevel = self.mainwindow.expertmode # daemon request ID of last command executed from this panel # (used to display messages from this command) self._current_status = 'idle' self._exec_reqid = None self._error_window = None client.connected.connect(self.on_client_connected) client.disconnected.connect(self.on_client_disconnected) client.cache.connect(self.on_client_cache) client.device.connect(self.on_client_device) client.setup.connect(self.on_client_setup) client.message.connect(self.on_client_message) self.filters = options.get('filters', []) self.filter.addItem('') for text, rx in self.filters: self.filter.addItem('Filter: %s' % text, rx) self.filter.lineEdit().setPlaceholderText('Enter search expression') def updateStatus(self, status, exception=False): self._current_status = status def saveSettings(self, settings): settings.setValue('headers', self.tree.header().saveState()) def loadSettings(self, settings): self._headerstate = settings.value('headers', '', QByteArray) def _update_view(self): with self.sgroup as settings: for i in range(self.tree.topLevelItemCount()): v = settings.value( '%s/expanded' % self.tree.topLevelItem(i).text(0), True, bool) self.tree.topLevelItem(i).setExpanded(v) def _store_view(self): with self.sgroup as settings: for i in range(self.tree.topLevelItemCount()): settings.setValue( '%s/expanded' % self.tree.topLevelItem(i).text(0), self.tree.topLevelItem(i).isExpanded()) def hideTitle(self): self.titleLbl.setVisible(False) def setExpertMode(self, expert): self._show_lowlevel = expert self.on_client_connected() def clear(self): if self.tree: self._store_view() self._catitems = {} # map lowercased devname -> tree widget item self._devitems = {} self._devparamitems = {} # map lowercased devname -> DevInfo instance self._devinfo = {} self.tree.clear() def on_client_connected(self): self.clear() state = self.client.ask('getstatus') if not state: return devlist = state['devices'] self._read_setup_info(state['setups']) for devname in devlist: self._create_device_item(devname) # close all control dialogs for now nonexisting devices for ldevname in list(self._control_dialogs): if ldevname not in self._devitems: self._control_dialogs[ldevname].close() # add all toplevel items to the tree, sorted for cat in self._catitems: self.tree.addTopLevelItem(self._catitems[cat]) self._catitems[cat].setExpanded(True) for devitem in itervalues(self._devitems): devitem.setExpanded(True) self.tree.sortItems(0, Qt.AscendingOrder) self._update_view() def on_client_disconnected(self): self.clear() def on_client_message(self, message): # show warnings and errors emitted by the current command in a window if message[5] != self._exec_reqid or message[2] < WARNING: return msg = '%s: %s' % (message[0], message[3].strip()) if self._error_window is None: def reset_errorwindow(): self._error_window = None self._error_window = ErrorDialog(self) self._error_window.accepted.connect(reset_errorwindow) self._error_window.addMessage(msg) self._error_window.show() else: self._error_window.addMessage(msg) self._error_window.activateWindow() def _read_setup_info(self, setuplists=None): if setuplists is None: allstatus = self.client.ask('getstatus') if allstatus is None: return setuplists = allstatus['setups'] loaded_setups = set(setuplists[0]) self._dev2setup = {} self._setupinfo = self.client.eval('session.getSetupInfo()', {}) if self._setupinfo is None: self.log.warning('session.getSetupInfo() returned None instead ' 'of {}') return for setupname, info in iteritems(self._setupinfo): if info is None: continue if setupname not in loaded_setups: continue for devname in info['devices']: self._dev2setup[devname] = setupname def _create_device_item(self, devname, add_cat=False): ldevname = devname.lower() # get all cache keys pertaining to the device params = self.client.getDeviceParams(devname) if not params: return lowlevel_device = params.get('lowlevel') or False if lowlevel_device and not self._show_lowlevel: return if 'nicos.core.data.sink.DataSink' in params.get('classes', []) and \ not self._show_lowlevel: return # remove still-existing previous item for the same device name if ldevname in self._devitems: self.on_client_device(('destroy', [devname])) cat = self._dev2setup.get(devname) if cat is None: # device is not in any setup? reread setup info self._read_setup_info() cat = self._dev2setup.get(devname) if cat is None: # still not there -> give up return if cat not in self._catitems: display_order = self._setupinfo[cat].get('display_order', 50) representative = self._setupinfo[cat].get('extended', {}).get( 'representative', '').lower() catitem = SetupTreeWidgetItem(cat, display_order, representative) catitem.setToolTip(0, self._setupinfo[cat].get('description', '')) f = catitem.font(0) f.setBold(True) catitem.setFont(0, f) catitem.setIcon(0, self.groupIcon) self._catitems[cat] = catitem if add_cat: self.tree.addTopLevelItem(catitem) catitem.setExpanded(True) else: catitem = self._catitems[cat] # create a tree node for the device devitem = QTreeWidgetItem(catitem, [devname, '', ''], DEVICE_TYPE) devitem.setForeground(0, lowlevelBrush[lowlevel_device]) devitem.setFont(0, lowlevelFont[lowlevel_device]) if self.useicons: devitem.setIcon(0, self.statusIcon[OK]) devitem.setToolTip(0, params.get('description', '')) self._devitems[ldevname] = devitem # fill the device info with dummy values, will be populated below self._devinfo[ldevname] = DevInfo(devname) # let the cache handler process all properties for key, value in iteritems(params): self.on_client_cache( (0, ldevname + '/' + key, OP_TELL, cache_dump(value))) def on_client_setup(self, setuplists): # update setup tooltips self._read_setup_info(setuplists) for i in range(self.tree.topLevelItemCount()): catitem = self.tree.topLevelItem(i) cat = catitem.text(0) catitem.setToolTip(0, self._setupinfo[cat].get('description', '')) def on_client_device(self, data): (action, devlist) = data if not devlist: return if action == 'create': for devname in devlist: self._create_device_item(devname, add_cat=True) self.tree.sortItems(0, Qt.AscendingOrder) self._update_view() elif action == 'destroy': self._store_view() for devname in devlist: ldevname = devname.lower() if ldevname in self._devitems: # remove device item and cached info... item = self._devitems[ldevname] del self._devitems[ldevname] del self._devinfo[ldevname] self._devparamitems.pop(ldevname, None) try: catitem = item.parent() except RuntimeError: # Qt object has already been destroyed pass else: catitem.removeChild(item) # remove category item if it has no further children if catitem.childCount() == 0: self.tree.takeTopLevelItem( self.tree.indexOfTopLevelItem(catitem)) del self._catitems[catitem.text(0)] self._update_view() def on_client_cache(self, data): (time, key, op, value) = data if '/' not in key: return ldevname, subkey = key.rsplit('/', 1) if ldevname not in self._devinfo: return if ldevname in self._control_dialogs: self._control_dialogs[ldevname].on_cache(subkey, value) devitem = self._devitems[ldevname] devinfo = self._devinfo[ldevname] if subkey == 'value': if time < devinfo.valtime: return if not value: fvalue = '' else: fvalue = cache_load(value) if isinstance(fvalue, list): fvalue = tuple(fvalue) devinfo.value = fvalue devinfo.expired = op != OP_TELL devinfo.valtime = time fmted = devinfo.fmtValUnit() devitem.setText(1, fmted) if ldevname in self._control_dialogs: self._control_dialogs[ldevname].valuelabel.setText(fmted) devitem.setForeground(1, valueBrush[devinfo.expired, devinfo.fixed]) if not devitem.parent().isExpanded(): if ldevname == devitem.parent().representative: devitem.parent().setText(1, fmted) elif subkey == 'status': if time < devinfo.stattime: return if not value: status = (UNKNOWN, '?') else: status = cache_load(value) devinfo.status = status devinfo.stattime = time devitem.setText(2, str(status[1])) if status[0] not in self.statusIcon: # old or wrong status constant return if self.useicons: devitem.setIcon(0, self.statusIcon[status[0]]) devitem.setForeground(2, foregroundBrush[status[0]]) devitem.setBackground(2, backgroundBrush[status[0]]) else: devitem.setForeground(0, foregroundBrush[BUSY]) devitem.setBackground(0, backgroundBrush[status[0]]) if not devitem.parent().isExpanded(): item = devitem.parent() item.setBackground( 0, backgroundBrush[self._getHighestStatus(item)]) else: devitem.parent().setBackground(0, backgroundBrush[OK]) if ldevname in self._control_dialogs: dlg = self._control_dialogs[ldevname] dlg.statuslabel.setText(status[1]) dlg.statusimage.setPixmap(self.statusIcon[status[0]].pixmap( 16, 16)) setForegroundBrush(dlg.statuslabel, foregroundBrush[status[0]]) setBackgroundBrush(dlg.statuslabel, backgroundBrush[status[0]]) elif subkey == 'fmtstr': if not value: return devinfo.fmtstr = cache_load(value) devitem.setText(1, devinfo.fmtValUnit()) elif subkey == 'unit': if not value: value = "''" devinfo.unit = cache_load(value) devitem.setText(1, devinfo.fmtValUnit()) elif subkey == 'fixed': if not value: value = "''" devinfo.fixed = bool(cache_load(value)) devitem.setForeground(1, valueBrush[devinfo.expired, devinfo.fixed]) if ldevname in self._control_dialogs: dlg = self._control_dialogs[ldevname] if dlg.moveBtn: dlg.moveBtn.setEnabled(not devinfo.fixed) dlg.moveBtn.setText(devinfo.fixed and '(fixed)' or 'Move') elif subkey == 'userlimits': if not value: return value = cache_load(value) if ldevname in self._control_dialogs: dlg = self._control_dialogs[ldevname] dlg.limitMin.setText(str(value[0])) dlg.limitMax.setText(str(value[1])) elif subkey == 'classes': if not value: value = "[]" devinfo.classes = set(cache_load(value)) elif subkey == 'alias': if not value: return if ldevname in self._control_dialogs: dlg = self._control_dialogs[ldevname] dlg._reinit() elif subkey == 'description': devitem.setToolTip(0, cache_load(value or "''")) if subkey in self.param_display.get(ldevname, ()): if not devinfo.params: devinfo.params = self.client.getDeviceParamInfo(devinfo.name) value = devinfo.fmtParam(subkey, cache_load(value)) if subkey not in self._devparamitems.setdefault(ldevname, {}): devitem = self._devitems[ldevname] self._devparamitems[ldevname][subkey] = \ QTreeWidgetItem(devitem, [subkey, value, ''], PARAM_TYPE) devitem.setExpanded(True) else: self._devparamitems[ldevname][subkey].setText(1, value) def on_tree_itemExpanded(self, item): if item.type() == SETUP_TYPE: item.setText(1, '') item.setBackground(0, backgroundBrush[OK]) def _getHighestStatus(self, item): retval = OK for i in range(item.childCount()): lstatus = self._devinfo[item.child(i).text(0).lower()].status[0] if retval < lstatus: retval = lstatus return retval def on_tree_itemCollapsed(self, item): if item.type() == SETUP_TYPE: item.setBackground(0, backgroundBrush[self._getHighestStatus(item)]) if item.representative: item.setText(1, self._devitems[item.representative].text(1)) def on_tree_customContextMenuRequested(self, point): item = self.tree.itemAt(point) if item is None: return if item.type() == DEVICE_TYPE: self._menu_dev = item.text(0) ldevname = self._menu_dev.lower() if 'nicos.core.device.Moveable' in self._devinfo[ldevname].classes and \ not self.client.viewonly: self.devmenu.popup(self.tree.viewport().mapToGlobal(point)) elif 'nicos.core.device.Readable' in self._devinfo[ ldevname].classes: self.devmenu_ro.popup(self.tree.viewport().mapToGlobal(point)) def on_filter_editTextChanged(self, text): for i in range(self.filter.count()): if text == self.filter.itemText(i): rx = QRegExp(self.filter.itemData(i)) break else: rx = QRegExp(text) for i in range(self.tree.topLevelItemCount()): setupitem = self.tree.topLevelItem(i) all_children_hidden = True for j in range(setupitem.childCount()): devitem = setupitem.child(j) if rx.indexIn(devitem.text(0)) == -1: devitem.setHidden(True) else: devitem.setHidden(False) all_children_hidden = False setupitem.setHidden(all_children_hidden) @pyqtSlot() def on_actionShutDown_triggered(self): if self._menu_dev: if self.askQuestion('This will unload the device until the setup ' 'is loaded again. Proceed?'): self.exec_command('RemoveDevice(%s)' % srepr(self._menu_dev), ask_queue=False) @pyqtSlot() def on_actionReset_triggered(self): if self._menu_dev: self.exec_command('reset(%s)' % srepr(self._menu_dev)) @pyqtSlot() def on_actionFix_triggered(self): if self._menu_dev: reason, ok = QInputDialog.getText( self, 'Fix', 'Please enter the reason for fixing %s:' % self._menu_dev) if not ok: return self.exec_command('fix(%s, %r)' % (srepr(self._menu_dev), reason)) @pyqtSlot() def on_actionRelease_triggered(self): if self._menu_dev: self.exec_command('release(%s)' % srepr(self._menu_dev)) @pyqtSlot() def on_actionStop_triggered(self): if self._menu_dev: self.exec_command('stop(%s)' % srepr(self._menu_dev), immediate=True) @pyqtSlot() def on_actionMove_triggered(self): if self._menu_dev: self._open_control_dialog(self._menu_dev) @pyqtSlot() def on_actionHelp_triggered(self): if self._menu_dev: self.client.eval('session.showHelp(session.devices[%r])' % self._menu_dev) @pyqtSlot() def on_actionPlotHistory_triggered(self): if self._menu_dev: self.plot_history(self._menu_dev) def on_tree_itemActivated(self, item, column): if item.type() == DEVICE_TYPE: devname = item.text(0) self._open_control_dialog(devname) elif item.type() == PARAM_TYPE: devname = item.parent().text(0) dlg = self._open_control_dialog(devname) dlg.editParam(item.text(0)) def _open_control_dialog(self, devname): ldevname = devname.lower() if ldevname in self._control_dialogs: dlg = self._control_dialogs[ldevname] if dlg.isVisible(): dlg.activateWindow() return dlg devinfo = self._devinfo[ldevname] item = self._devitems[ldevname] dlg = ControlDialog(self, devname, devinfo, item, self.log, self._show_lowlevel) dlg.closed.connect(self._control_dialog_closed) dlg.rejected.connect(dlg.close) self._control_dialogs[ldevname] = dlg dlg.show() return dlg def _control_dialog_closed(self, ldevname): dlg = self._control_dialogs.pop(ldevname, None) if dlg: dlg.deleteLater() # API shared with ControlDialog def exec_command(self, command, ask_queue=True, immediate=False): if ask_queue and not immediate and self._current_status != 'idle': qwindow = ScriptExecQuestion() result = qwindow.exec_() if result == QMessageBox.Cancel: return elif result == QMessageBox.Apply: immediate = True if immediate: self.client.tell('exec', command) self._exec_reqid = None # no request assigned to this command else: self._exec_reqid = self.client.run(command) def plot_history(self, dev): if self.mainwindow.history_wintype is not None: win = self.mainwindow.createWindow(self.mainwindow.history_wintype) if win: panel = win.getPanel('History viewer') panel.newView(dev) showPanel(panel)
class AmorControlPanel(GenericPanel): def __init__(self, parent, client, options): GenericPanel.__init__(self, parent, client, options) for ch in self.findChildren(NicosWidget): ch.setClient(client) # daemon request ID of last command executed from this panel # (used to display messages from this command) self._current_status = 'idle' self._exec_reqid = None self._error_window = None self.motor_widgets = { 'som': self.somEdit, 's2t': self.s2tEdit, 'soz': self.sozEdit, 'stz': self.stzEdit } self.slit_widgets = { 'slit1_opening': self.slit1Edit, 'slit2_opening': self.slit2Edit, 'slit3_opening': self.slit3Edit, 'slit4_opening': self.slit4Edit, 'd5v': self.slit5Edit, } self.magnet_widgets = {'hsy': self.hsyEdit} # Initialise the widgets self._reinit() # Set the validator for monitor preset edit # Regular expression for scientific notation: [\d.]+(?:e\d+)? self.monitorPresetBox.setValidator( QRegExpValidator(QRegExp(r"[\d.]+(?:e\d+)?"), self)) self.setMonitorPreset(True) self.setSampleMove(True) self.opMonitor.toggled.connect(self.setMonitorPreset) self.opTime.toggled.connect(self.setTimePreset) self.opSampleMove.toggled.connect(self.setSampleMove) self.opSampleSetPosition.toggled.connect(self.setSampleSetPosition) client.message.connect(self.on_client_message) client.setup.connect(self.on_client_setup) def updateStatus(self, status, exception=False): self._current_status = status def _reinit(self): widgets_dict = {} widgets_dict.update(self.motor_widgets) widgets_dict.update(self.slit_widgets) widgets_dict.update(self.magnet_widgets) for n, w in widgets_dict.items(): currval = self.client.getDeviceParam(n, 'value') w._reinit(currval) def on_client_setup(self): self._reinit() def on_client_message(self, message): # show warnings and errors emitted by the current command in a window if message[5] != self._exec_reqid or message[2] < WARNING: return msg = '%s: %s' % (message[0], message[3].strip()) if self._error_window is None: def reset_errorwindow(): self._error_window = None self._error_window = ErrorDialog(self) self._error_window.accepted.connect(reset_errorwindow) self._error_window.addMessage(msg) self._error_window.show() else: self._error_window.addMessage(msg) self._error_window.activateWindow() self._reinit() def setMonitorPreset(self, checked): self.timePresetWidget.setVisible(not checked) self.monitorPresetWidget.setVisible(checked) def setTimePreset(self, checked): self.monitorPresetWidget.setVisible(not checked) self.timePresetWidget.setVisible(checked) def setSampleMove(self, checked): self.sampleSetPositionButtonWidget.setVisible(not checked) self.motor_widgets['s2t'].setEnabled(checked) self.sampleMoveButtonWidget.setVisible(checked) self._reinit() def setSampleSetPosition(self, checked): self.motor_widgets['s2t'].setEnabled(not checked) self.sampleMoveButtonWidget.setVisible(not checked) self.sampleSetPositionButtonWidget.setVisible(checked) self._reinit() def exec_command(self, command, ask_queue=True, immediate=False): if ask_queue and not immediate and self._current_status != 'idle': qwindow = ScriptExecQuestion() result = qwindow.exec_() if result == QMessageBox.Cancel: return elif result == QMessageBox.Apply: immediate = True if immediate: self.client.tell('exec', command) self._exec_reqid = None # no request assigned to this command else: self._exec_reqid = self.client.run(command) @pyqtSlot() def on_countStartButton_clicked(self): if self.opTime.isChecked(): value = float(self.timePresetBox.text()) preset = 't' else: try: value = float(self.monitorPresetBox.text()) preset = 'm' except ValueError: self.log.exception('invalid value for monitor preset') QMessageBox.warning(self, 'Error', 'Invalid monitor preset') return args = '%s=%r' % (preset, int(value)) code = 'count(%s)' % args self.exec_command(code) @pyqtSlot() def on_sampleMoveButton_clicked(self): self._devsMoveButton(self.motor_widgets, 'move') @pyqtSlot() def on_sampleMoveAndWaitButton_clicked(self): self._devsMoveButton(self.motor_widgets, 'maw') @pyqtSlot() def on_sampleSetPositionButton_clicked(self): self._devsMoveButton(self.motor_widgets, 'adjust', True) @pyqtSlot() def on_slitMoveButton_clicked(self): self._devsMoveButton(self.slit_widgets, 'move') @pyqtSlot() def on_slitMoveAndWaitButton_clicked(self): self._devsMoveButton(self.slit_widgets, 'maw') @pyqtSlot() def on_hsyToggleButton_clicked(self): switchedOn = self.client.eval('hsy_switch.isEnabled', None) if switchedOn is None: self.showError('Cannot check the status of magnets!') newstate = 'off' if switchedOn else 'on' code = 'hsy_switch.move(%r)' % newstate self.exec_command(code) @pyqtSlot() def on_hsyMoveButton_clicked(self): self._devsMoveButton(self.magnet_widgets, 'move') def _devsMoveButton(self, dic, cmd='move', issue_separate=False): dev_to_widget = { n: w for n, w in dic.items() if not isinstance(w._inner, MissingWidget) } targets = [] try: targets = [edit.getValue() for edit in dev_to_widget.values()] except ValueError: self.log.exception('invalid value for typed value') # shouldn't happen, but if it does, at least gives indication that # something went wrong QMessageBox.warning(self, 'Error', 'Some entered value is invalid') return # Check which motors to move expr = ', '.join([ n + '.isAtTarget(target=%r)' % v for n, v in zip(dev_to_widget, targets) ]) on_targ = self.client.eval(expr, None) if on_targ is None: self.showError('Cannot check the status! Cannot move!') return if not isinstance(on_targ, tuple): on_targ = [on_targ] move = { n: v for n, v, t in zip(dev_to_widget, targets, on_targ) if not t } if not move: return if issue_separate: code = '\n'.join( ('%s(%s, %r)' % (cmd, n, v) for n, v in move.items())) else: code = '%s(%s)' % (cmd, ', '.join( ('%s, %r' % (n, v) for n, v in move.items()))) self.exec_command(code)
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