def __init__(self, parent, client, options): LokiPanelBase.__init__(self, parent, client, options) loadUi(self, findResource('nicos_ess/loki/gui/ui_files/sampleconf.ui')) self.sampleGroup.setEnabled(False) self.frame.setLayout(QVBoxLayout()) self.sample_frame = QFrame(self) loadUi( self.sample_frame, findResource('nicos_ess/loki/gui/ui_files/sampleconf_summary.ui')) layout = self.frame.layout() layout.addWidget(self.sample_frame) self.sample_frame.hide() self.sample_frame.posTbl.setEnabled(False) for box in self.sample_frame.findChildren(QLineEdit): box.setEnabled(False) menu = QMenu(self) menu.addAction(self.actionEmpty) menu.addAction(self.actionGenerate) self.createBtn.setMenu(menu) self.configs = [] self.holder_info = options.get('holder_info', []) self.instrument = options.get('instrument', 'loki') self.unapplied_changes = False self.applyBtn.setEnabled(False) self.initialise_connection_status_listeners()
def on_tableWidget_customContextMenuRequested(self, point): """Show context menu for adding and deleting echotimes (rows).""" self._stop_edit() menu = QMenu(self) add = menu.addAction('Add echo time') delete = menu.addAction('Delete echo time') row = self.tableWidget.rowAt(point.y()) # Disable the delete action if there is nothing to delete if row == -1: delete.setEnabled(False) sender = self.sender() # The Signal can be sent from the table widget itself or from the # vertical header. In case of the table widget, its viewport hast to be # used for correct placement of the context mneu if hasattr(sender, 'viewport'): sender = sender.viewport() action = menu.exec_(sender.mapToGlobal(point)) if action == add: self._add_row(row) elif action == delete: self._delete_row(row)
def _refresh_presets(self): pmenu = self.presetmenu pmenu.clear() delmenu = QMenu('Delete', self) try: for preset, info in iteritems(self.presetdict): paction = QAction(preset, self) pdelaction = QAction(preset, self) info = info.copy() def launchpreset(on, info=info): self._createViewFromDialog(info) def delpreset(on, name=preset, act=paction, delact=pdelaction): pmenu.removeAction(act) delmenu.removeAction(delact) self.presetdict.pop(name, None) self._refresh_presets() paction.triggered[bool].connect(launchpreset) pmenu.addAction(paction) pdelaction.triggered[bool].connect(delpreset) delmenu.addAction(pdelaction) except AttributeError: self.presetdict = {} if self.presetdict: pmenu.addSeparator() pmenu.addMenu(delmenu) else: pmenu.addAction('(no presets created)')
def on_roiItemClicked(self, event): if event.getButtons() & MouseEvent.RIGHT_BUTTON: if isinstance(event.roi.reference, FitResult): menu = QMenu(self) actionClipboard = QAction("Copy fit values to clipboard", menu) menu.addAction(actionClipboard) p0dc = event.getDC() selectedItem = menu.exec_( self.mapToGlobal(QPoint(p0dc.x, p0dc.y))) if selectedItem == actionClipboard: res = event.roi.reference text = '\n'.join( (n + '\t' if n else '\t') + (v + '\t' if isinstance(v, string_types) else '%g\t' % v) + (dv if isinstance(dv, string_types) else '%g' % dv) for (n, v, dv) in res.label_contents) QApplication.clipboard().setText(text)
def on_paramList_customContextMenuRequested(self, pos): item = self.paramList.itemAt(pos) if not item: return menu = QMenu(self) refreshAction = menu.addAction('Refresh') menu.addAction('Refresh all') # QCursor.pos is more reliable then the given pos action = menu.exec_(QCursor.pos()) if action: cmd = 'session.getDevice(%r).pollParams(volatile_only=False%s)' \ % (self.devname, ', param_list=[%r]' % item.text(0) if action == refreshAction else '') # poll even non volatile parameter as requested explicitely self.client.eval(cmd, None)
def __init__(self, log, gui_conf, viewonly=False, tunnel=''): DefaultMainWindow.__init__(self, log, gui_conf, viewonly, tunnel) self.addLogo() self.addInstrument() self.addExperiment() self.set_icons() self.stylefile = gui_conf.stylefile # Cheesburger menu dropdown = QMenu('') dropdown.addAction(self.actionConnect) dropdown.addAction(self.actionViewOnly) dropdown.addAction(self.actionPreferences) dropdown.addAction(self.actionExpert) dropdown.addSeparator() dropdown.addAction(self.actionExit) self.actionUser.setMenu(dropdown) self.actionUser.setIconVisibleInMenu(True) self.dropdown = dropdown
def __init__(self, log, gui_conf, viewonly=False, tunnel=''): DefaultMainWindow.__init__(self, log, gui_conf, viewonly, tunnel) self.add_logo() self.set_icons() self.style_file = gui_conf.stylefile # Cheeseburger menu dropdown = QMenu('') dropdown.addAction(self.actionConnect) dropdown.addAction(self.actionViewOnly) dropdown.addAction(self.actionPreferences) dropdown.addAction(self.actionExpert) dropdown.addSeparator() dropdown.addAction(self.actionExit) self.actionUser.setMenu(dropdown) self.actionUser.setIconVisibleInMenu(True) self.dropdown = dropdown self.actionExpert.setEnabled(self.client.isconnected) self.actionEmergencyStop.setEnabled(self.client.isconnected) self._init_instrument_name() self._init_experiment_name()
def contextMenuOnItem(self, item, pos): if item is None: return # invoked context menu on whitespace if item.type() == ItemTypes.Setup: menu = QMenu(self) addDeviceAction = menu.addAction('Add device...') addDeviceAction.triggered.connect(self.addDevice) menu.popup(pos) elif item.type() == ItemTypes.Directory: menu = QMenu(self) addSetupAction = menu.addAction('Add setup...') addSetupAction.triggered.connect(self.addSetup) menu.popup(pos) elif item.type() == ItemTypes.Device: menu = QMenu(self) removeDeviceAction = menu.addAction('Remove') removeDeviceAction.triggered.connect(self.removeDevice) menu.popup(pos)
def getMenus(self): if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionPrint) menu.addSeparator() menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) self.menu = menu return [self.menu]
def contextMenuOnItem(self, item, pos): if item is None: return # invoked context menu on whitespace topLevelItems = [] currentIndex = 0 while currentIndex < self.topLevelItemCount(): topLevelItems.append(self.topLevelItem(currentIndex)) currentIndex += 1 if self.currentItem() in topLevelItems: if self.currentItem().text(0) in self.nonListItems: if self.currentItem().childCount() > 0: return # value is already set, can't add multiple values menu = QMenu(self) addValueAction = menu.addAction('Add value...') addValueAction.triggered.connect(self.addValue) menu.popup(pos)
def _show_context_menu(self): menu = QMenu() copy_action = QAction("Copy", self) copy_action.triggered.connect(self._handle_copy_cells) copy_action.setIcon(get_icon("file_copy-24px.svg")) menu.addAction(copy_action) cut_action = QAction("Cut", self) cut_action.triggered.connect(self._handle_cut_cells) cut_action.setIcon(get_icon("cut_24px.svg")) menu.addAction(cut_action) paste_action = QAction("Paste", self) paste_action.triggered.connect(self._handle_table_paste) paste_action.setIcon(get_icon("paste_24px.svg")) menu.addAction(paste_action) delete_action = QAction("Delete", self) delete_action.triggered.connect(self._delete_rows) delete_action.setIcon(get_icon("remove-24px.svg")) menu.addAction(delete_action) menu.exec_(QCursor.pos())
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, findResource('nicos_mlz/kws1/gui/sampleconf.ui')) self.sampleGroup.setEnabled(False) self.frame.setLayout(QVBoxLayout()) menu = QMenu(self) menu.addAction(self.actionCopyAperture) menu.addAction(self.actionCopyDetOffset) menu.addAction(self.actionCopyThickness) menu.addAction(self.actionCopyTimeFactor) menu.addSeparator() menu.addAction(self.actionCopyAll) self.copyBtn.setMenu(menu) menu = QMenu(self) menu.addAction(self.actionEmpty) menu.addAction(self.actionGenerate) self.createBtn.setMenu(menu) self.configs = [] self.dirty = False self.filename = None self.holder_info = options.get('holder_info', []) self.instrument = options.get('instrument', 'kws1')
def getMenus(self): menu = QMenu('&History viewer', self) menu.addAction(self.actionNew) menu.addSeparator() menu.addAction(self.actionSavePlot) menu.addAction(self.actionPrint) menu.addAction(self.actionAttachElog) menu.addAction(self.actionSaveData) menu.addSeparator() menu.addAction(self.actionEditView) menu.addAction(self.actionCloseView) menu.addAction(self.actionDeleteView) menu.addAction(self.actionResetView) menu.addSeparator() menu.addAction(self.actionLogScale) menu.addAction(self.actionAutoScale) menu.addAction(self.actionScaleX) menu.addAction(self.actionScaleY) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLegend) menu.addAction(self.actionSymbols) menu.addAction(self.actionLines) ag = QActionGroup(menu) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) menu.addAction(self.actionFitPeak) menu.addAction(self.actionPickInitial) menu.addAction(self.actionFitPeakGaussian) menu.addAction(self.actionFitPeakLorentzian) menu.addAction(self.actionFitPeakPV) menu.addAction(self.actionFitPeakPVII) menu.addAction(self.actionFitTc) menu.addAction(self.actionFitCosine) menu.addAction(self.actionFitSigmoid) menu.addAction(self.actionFitLinear) menu.addAction(self.actionFitExponential) menu.addSeparator() menu.addAction(self.actionFitArby) menu.addSeparator() menu.addAction(self.actionClose) self._refresh_presets() return [menu, self.presetmenu]
class DevicesPanel1(DevicesPanel): 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 charmpowersupply in self._devinfo[ldevname].classes: params = self.client.getDeviceParams(ldevname) self.cpsmenu = QMenu() self.cps_actions = [] i = 0 for menuItem in params['transitions']: self.cps_actions.append(QAction(menuItem)) self.cpsmenu.addAction(self.cps_actions[i]) self.cps_actions[i].triggered.connect( partial(self.on_actionApply_triggered, i)) i = i + 1 self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionMove) self.cpsmenu.addAction(self.actionReset) self.cpsmenu.addSeparator() if self.mainwindow.history_wintype is not None: self.cpsmenu.addAction(self.actionPlotHistory) self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionShutDown) self.cpsmenu.addAction(self.actionHelp) self.cpsmenu.popup(self.tree.viewport().mapToGlobal(point)) return if roimanager in self._devinfo[ldevname].classes: params = self.client.getDeviceParams(ldevname) self.cpsmenu = QMenu() self.cps_actions = [] self.cps_actions.append(QAction('Edit...')) self.cpsmenu.addAction(self.cps_actions[0]) self.cps_actions[0].triggered.connect( partial(self.on_roimanagerEdit, ldevname)) self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionMove) self.cpsmenu.addAction(self.actionReset) self.cpsmenu.addSeparator() if self.mainwindow.history_wintype is not None: self.cpsmenu.addAction(self.actionPlotHistory) self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionShutDown) self.cpsmenu.addAction(self.actionHelp) self.cpsmenu.popup(self.tree.viewport().mapToGlobal(point)) return if compareimage in self._devinfo[ldevname].classes: params = self.client.getDeviceParams(ldevname) self.cpsmenu = QMenu() self.cps_actions = [] self.cps_actions.append(QAction('Show &Compare...')) self.cpsmenu.addAction(self.cps_actions[0]) self.cps_actions[0].triggered.connect( partial(self.on_images_compare, ldevname)) self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionMove) self.cpsmenu.addAction(self.actionReset) self.cpsmenu.addSeparator() if self.mainwindow.history_wintype is not None: self.cpsmenu.addAction(self.actionPlotHistory) self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionShutDown) self.cpsmenu.addAction(self.actionHelp) self.cpsmenu.popup(self.tree.viewport().mapToGlobal(point)) return if playlistmanager in self._devinfo[ldevname].classes: params = self.client.getDeviceParams(ldevname) self.cpsmenu = QMenu() self.cps_actions = [] self.cps_actions.append(QAction('Edit...')) self.cpsmenu.addAction(self.cps_actions[0]) self.cps_actions[0].triggered.connect( partial(self.on_playlist_edit, ldevname)) self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionMove) self.cpsmenu.addAction(self.actionReset) self.cpsmenu.addSeparator() if self.mainwindow.history_wintype is not None: self.cpsmenu.addAction(self.actionPlotHistory) self.cpsmenu.addSeparator() self.cpsmenu.addAction(self.actionShutDown) self.cpsmenu.addAction(self.actionHelp) self.cpsmenu.popup(self.tree.viewport().mapToGlobal(point)) return return super().on_tree_customContextMenuRequested(point) @pyqtSlot() def on_playlist_edit(self, ldevname): if not playlisteditor.win: playlisteditor.win = playlisteditor.Window(self.client, ldevname) playlisteditor.win.show() @pyqtSlot() def on_images_compare(self, ldevname): if not imagecompare.win: imagecompare.win = imagecompare.Window(self.client, ldevname) imagecompare.win.show() @pyqtSlot() def on_roimanagerEdit(self, ldevname): if not roieditor.win: roieditor.win = roieditor.Window(self.client, ldevname) roieditor.win.show() @pyqtSlot() def on_actionApply_triggered(self, index): if self._menu_dev: self.client.eval(self._menu_dev + '.apply(' + str(index) + ')') def on_client_cache(self, data): rv = super().on_client_cache(data) # here we truncate the long status message for charmpowersupply devices (time, key, op, value) = data if '/' not in key: return ldevname, subkey = key.rsplit('/', 1) if ldevname not in self._devinfo: return devitem = self._devitems[ldevname] devinfo = self._devinfo[ldevname] if charmpowersupply in self._devinfo[ldevname].classes: if subkey == 'status': t = devitem.text(2) i = t.rfind('[') if i >= 0: t = t[:i - 1] devitem.setText(2, str(t)) if ldevname in self._control_dialogs: dlg = self._control_dialogs[ldevname] ct = dlg.statuslabel.text() i = ct.rfind('[') if i >= 0: ct = ct[:i - 1] dlg.statuslabel.setText(ct) return rv
def getMenus(self): if self._liveOnlyIndex is not None: return [] if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionOpen) menu.addAction(self.actionPrint) menu.addAction(self.actionSavePlot) menu.addSeparator() menu.addAction(self.actionKeepRatio) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionColormap) menu.addAction(self.actionMarkCenter) menu.addAction(self.actionROI) menu.addAction(self.actionSymbols) menu.addAction(self.actionLines) self.menu = menu return [self.menu]
def _reinit(self): classes = self.devinfo.classes if sip.isdeleted(self.devitem): # The item we're controlling has been removed from the list (e.g. # due to client reconnect), get it again. self.devitem = self.device_panel._devitems.get( self.devname.lower()) # No such device anymore... if self.devitem is None: self.close() return self.deviceName.setText('Device: %s' % self.devname) self.setWindowTitle('Control %s' % self.devname) self.settingsBtn = self.buttonBox.button( QDialogButtonBox.RestoreDefaults) self.settingsBtn.clicked.connect(self.on_settingsBtn_clicked) # trigger parameter poll self.client.eval('%s.pollParams()' % self.devname, None) # now get all cache keys pertaining to the device and set the # properties we want params = self.client.getDeviceParams(self.devname) self.paraminfo = self.client.getDeviceParamInfo(self.devname) self.paramvalues = dict(params) # put parameter values in the list widget self.paramItems.clear() self.paramList.clear() for key, value in sorted(iteritems(params)): if self.paraminfo.get(key): # normally, show only userparams, except in expert mode is_userparam = self.paraminfo[key]['userparam'] if is_userparam or self.device_panel._show_lowlevel: self.paramItems[key] = item = \ QTreeWidgetItem(self.paramList, [key, str(value)]) # display non-userparams in grey italics, like lowlevel # devices in the device list if not is_userparam: item.setFont(0, lowlevelFont[True]) item.setForeground(0, lowlevelBrush[True]) # set description label if params.get('description'): self.description.setText(params['description']) else: self.description.setVisible(False) # check how to refer to the device in commands: if it is lowlevel, # we need to use quotes self.devrepr = srepr(self.devname) if params.get('lowlevel', True) \ else self.devname # show "Set alias" group box if it is an alias device if 'alias' in params: if params['alias']: self.deviceName.setText(self.deviceName.text() + ' (alias for %s)' % params['alias']) alias_config = self.client.eval('session.alias_config', {}) self.aliasTarget = QComboBox(self) self.aliasTarget.setEditable(True) if self.devname in alias_config: items = [t[0] for t in alias_config[self.devname]] self.aliasTarget.addItems(items) if params['alias'] in items: self.aliasTarget.setCurrentIndex( items.index(params['alias'])) self.targetLayoutAlias.takeAt(1).widget().deleteLater() self.targetLayoutAlias.insertWidget(1, self.aliasTarget) if self.client.viewonly: self.setAliasBtn.setEnabled(False) else: self.aliasGroup.setVisible(False) historyBtn = self.buttonBox.button(QDialogButtonBox.Reset) # show current value/status if it is readable if 'nicos.core.device.Readable' not in classes: self.valueFrame.setVisible(False) self.buttonBox.removeButton(historyBtn) else: self.valuelabel.setText(self.devitem.text(1)) self.statuslabel.setText(self.devitem.text(2)) self.statusimage.setPixmap(self.devitem.icon(0).pixmap(16, 16)) setForegroundBrush(self.statuslabel, self.devitem.foreground(2)) setBackgroundBrush(self.statuslabel, self.devitem.background(2)) # modify history button: add icon and set text historyBtn.setIcon(QIcon(':/find')) historyBtn.setText('Plot history...') historyBtn.clicked.connect(self.on_historyBtn_clicked) if self.client.viewonly: self.limitFrame.setVisible(False) self.targetFrame.setVisible(False) return # add a menu for the "More" button self.moveBtns.clear() menu = QMenu(self) if 'nicos.core.mixins.HasLimits' in classes: menu.addAction(self.actionSetLimits) if 'nicos.core.mixins.HasOffset' in classes: menu.addAction(self.actionAdjustOffset) if 'nicos.devices.abstract.CanReference' in classes: menu.addAction(self.actionReference) if 'nicos.devices.abstract.Coder' in classes: menu.addAction(self.actionSetPosition) if 'nicos.core.device.Moveable' in classes: if not menu.isEmpty(): menu.addSeparator() menu.addAction(self.actionFix) menu.addAction(self.actionRelease) if 'nicos.core.mixins.CanDisable' in classes: if not menu.isEmpty(): menu.addSeparator() menu.addAction(self.actionEnable) menu.addAction(self.actionDisable) if not menu.isEmpty(): menuBtn = QPushButton('More', self) menuBtn.setMenu(menu) self.moveBtns.addButton(menuBtn, QDialogButtonBox.ResetRole) def reset(checked): self.device_panel.exec_command('reset(%s)' % self.devrepr) def stop(checked): self.device_panel.exec_command('stop(%s)' % self.devrepr, immediate=True) self.moveBtns.addButton('Reset', QDialogButtonBox.ResetRole)\ .clicked.connect(reset) if 'nicos.core.device.Moveable' in classes or \ 'nicos.core.device.Measurable' in classes: self.moveBtns.addButton('Stop', QDialogButtonBox.ResetRole)\ .clicked.connect(stop) # show target and limits if the device is Moveable if 'nicos.core.device.Moveable' not in classes: self.limitFrame.setVisible(False) self.targetFrame.setVisible(False) else: if 'nicos.core.mixins.HasLimits' not in classes: self.limitFrame.setVisible(False) else: self.limitMin.setText(str(params['userlimits'][0])) self.limitMax.setText(str(params['userlimits'][1])) # insert a widget to enter a new device value # allowEnter=False because we catch pressing Enter ourselves self.target = DeviceValueEdit(self, dev=self.devname, useButtons=True, allowEnter=False) self.target.setClient(self.client) def btn_callback(target): self.device_panel.exec_command('move(%s, %s)' % (self.devrepr, srepr(target))) self.target.valueChosen.connect(btn_callback) self.targetFrame.layout().takeAt(1).widget().deleteLater() self.targetFrame.layout().insertWidget(1, self.target) def move(checked): try: target = self.target.getValue() except ValueError: return self.device_panel.exec_command('move(%s, %s)' % (self.devrepr, srepr(target))) if self.target.getValue() is not Ellipsis: # (button widget) self.moveBtn = self.moveBtns.addButton( 'Move', QDialogButtonBox.AcceptRole) self.moveBtn.clicked.connect(move) else: self.moveBtn = None if params.get('fixed') and self.moveBtn: self.moveBtn.setEnabled(False) self.moveBtn.setText('(fixed)')
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 LiveDataPanel(Panel): """Provides a generic "detector live view". For most instruments, a specific panel must be implemented that takes care of the individual live display needs. Options: * ``filetypes`` (default []) - List of filename extensions whose content should be displayed. * ``detectors`` (default []) - List of detector devices whose data should be displayed. If not set data from all configured detectors will be shown. * ``cachesize`` (default 20) - Number of entries in the live data cache. The live data cache allows displaying of previously measured data. * ``liveonlyindex`` (default None) - Enable live only view. This disables interaction with the liveDataPanel and only displays the dataset of the set index. * ``defaults`` (default []) - List of strings representing options to be set for every configured plot. These options can not be set on a per plot basis since they are global. Options are as follows: * ``logscale`` - Switch the logarithic scale on. * ``center`` - Display the center lines for the image. * ``nolines`` - Display lines for the curve. * ``markers`` - Display symbols for data points. * ``unzoom`` - Unzoom the plot when new data is received. * ``plotsettings`` (default []) - List of dictionaries which contain settings for the individual datasets. Each entry will be applied to one of the detector's datasets. * ``plotcount`` (default 1) - Amount of plots in the dataset. * ``marks`` (default 'omark') - Shape of the markers (if displayed). Possible values are: 'dot', 'plus', 'asterrisk', 'circle', 'diagonalcross', 'solidcircle', 'triangleup', 'solidtriangleup', 'triangledown', 'solidtriangledown', 'square', 'solidsquare', 'bowtie', 'solidbowtie', 'hourglass', 'solidhourglass', 'diamond', 'soliddiamond', 'star', 'solidstar', 'triupdown', 'solidtriright', 'solidtrileft', 'hollowplus', 'solidplus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'star4', 'star5', 'star6', 'star7', 'star8', 'vline', 'hline', 'omark' * ``markersize`` (default 1) - Size of the markers (if displayed). * ``offset`` (default 0) - Offset for the X axis labels of each curve in 1D plots. * ``colors`` (default ['blue']) - Color of the marks and lines (if displayed). If colors are set as a list the colors will be applied to the individual plots (and default back to blue when wrong/missing), for example: ['red', 'green']: The first plot will be red, the second green and the others will be blue (default). 'red': all plots will be red. """ panelName = 'Live data view' ui = f'{uipath}/panels/live.ui' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, self.ui) self._allowed_filetypes = set() self._allowed_detectors = set() self._runtime = 0 self._range_active = False self._cachesize = 20 self._livewidgets = {} # livewidgets for rois: roi_key -> widget self._fileopen_filter = None self.widget = None self.menu = None self.unzoom = False self.lastSettingsIndex = None self._axis_labels = {} self.params = {} self._offset = 0 self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.toolbar = QToolBar('Live data') self.toolbar.addAction(self.actionOpen) self.toolbar.addAction(self.actionPrint) self.toolbar.addAction(self.actionSavePlot) self.toolbar.addSeparator() self.toolbar.addAction(self.actionLogScale) self.toolbar.addSeparator() self.toolbar.addAction(self.actionKeepRatio) self.toolbar.addAction(self.actionUnzoom) self.toolbar.addAction(self.actionColormap) self.toolbar.addAction(self.actionMarkCenter) self.toolbar.addAction(self.actionROI) self._actions2D = [self.actionROI, self.actionColormap] self.setControlsEnabled(False) self.set2DControlsEnabled(False) # hide fileselection in liveonly mode self._liveOnlyIndex = options.get('liveonlyindex', None) if self._liveOnlyIndex is not None: self.pastFilesWidget.hide() self.statusBar.hide() # disable interactions with the plot self.setAttribute(Qt.WA_TransparentForMouseEvents) self.liveitems = [] self.setLiveItems(1) self._livechannel = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if hasattr(self.window(), 'closed'): self.window().closed.connect(self.on_closed) client.livedata.connect(self.on_client_livedata) client.connected.connect(self.on_client_connected) client.cache.connect(self.on_cache) self.rois = {} self.detectorskey = None # configure allowed file types supported_filetypes = ReaderRegistry.filetypes() opt_filetypes = set(options.get('filetypes', supported_filetypes)) self._allowed_filetypes = opt_filetypes & set(supported_filetypes) # configure allowed detector device names detectors = options.get('detectors') if detectors: self._allowed_detectors = set(detectors) defaults = options.get('defaults', []) if 'logscale' in defaults: self.actionLogScale.setChecked(True) if 'center' in defaults: self.actionMarkCenter.setChecked(True) if 'nolines' not in defaults: self.actionLines.setChecked(True) if 'markers' in defaults: self.actionSymbols.setChecked(True) if 'unzoom' in defaults: self.unzoom = True self.plotsettings = options.get('plotsettings', [DEFAULTS]) # configure caching self._cachesize = options.get('cachesize', self._cachesize) if self._cachesize < 1 or self._liveOnlyIndex is not None: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize) self._initControlsGUI() def _initControlsGUI(self): pass def setLiveItems(self, n): nitems = len(self.liveitems) if n < nitems: nfiles = self.fileList.count() for i in range(nitems - 1, n - 1, -1): self.liveitems.pop(i) self.fileList.takeItem(nfiles - nitems + i) if self._livechannel > n: self._livechannel = 0 if n > 0 else None else: for i in range(nitems, n): item = QListWidgetItem('<Live #%d>' % (i + 1)) item.setData(FILENAME, i) item.setData(FILETYPE, '') item.setData(FILETAG, LIVE) self.fileList.insertItem(self.fileList.count(), item) self.liveitems.append(item) if self._liveOnlyIndex is not None: self.fileList.setCurrentRow(self._liveOnlyIndex) if n == 1: self.liveitems[0].setText('<Live>') else: self.liveitems[0].setText('<Live #1>') def set2DControlsEnabled(self, flag): if flag != self.actionKeepRatio.isChecked(): self.actionKeepRatio.trigger() for action in self._actions2D: action.setVisible(flag) def setControlsEnabled(self, flag): for action in self.toolbar.actions(): action.setEnabled(flag) self.actionOpen.setEnabled(True) # File Open action always available def initLiveWidget(self, widgetcls): if isinstance(self.widget, widgetcls): return # delete the old widget if self.widget: self.widgetLayout.removeWidget(self.widget) self.widget.deleteLater() # create a new one self.widget = widgetcls(self) # enable/disable controls and set defaults for new livewidget instances self.setControlsEnabled(True) if isinstance(self.widget, LiveWidget1D): self.set2DControlsEnabled(False) else: self.set2DControlsEnabled(True) # apply current global settings self.widget.setCenterMark(self.actionMarkCenter.isChecked()) self.widget.logscale(self.actionLogScale.isChecked()) if isinstance(self.widget, LiveWidget1D): self.widget.setSymbols(self.actionSymbols.isChecked()) self.widget.setLines(self.actionLines.isChecked()) # liveonly mode does not display a status bar if self._liveOnlyIndex is None: self.widget.gr.cbm.addHandler(MouseEvent.MOUSE_MOVE, self.on_mousemove_gr) # handle menus self.menuColormap = QMenu(self) self.actionsColormap = QActionGroup(self) activeMap = self.widget.getColormap() activeCaption = None for name, value in COLORMAPS.items(): caption = name.title() action = self.menuColormap.addAction(caption) action.setData(caption) action.setCheckable(True) if activeMap == value: action.setChecked(True) # update toolButton text later otherwise this may fail # depending on the setup and qt versions in use activeCaption = caption self.actionsColormap.addAction(action) action.triggered.connect(self.on_colormap_triggered) self.actionColormap.setMenu(self.menuColormap) # finish initiation self.widgetLayout.addWidget(self.widget) if activeCaption: self.toolbar.widgetForAction( self.actionColormap).setText(activeCaption) detectors = self.client.eval('session.experiment.detectors', []) self._register_rois(detectors) def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('geometry', self.saveGeometry()) def getMenus(self): if self._liveOnlyIndex is not None: return [] if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionOpen) menu.addAction(self.actionPrint) menu.addAction(self.actionSavePlot) menu.addSeparator() menu.addAction(self.actionKeepRatio) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionColormap) menu.addAction(self.actionMarkCenter) menu.addAction(self.actionROI) menu.addAction(self.actionSymbols) menu.addAction(self.actionLines) self.menu = menu return [self.menu] def _get_all_widgets(self): yield self.widget yield from self._livewidgets.values() def getToolbars(self): if self._liveOnlyIndex is not None: return [] return [self.toolbar] def on_mousemove_gr(self, event): xyz = None if event.getWindow(): # inside plot xyz = self.widget.getZValue(event) if xyz: fmt = '(%g, %g)' # x, y data 1D integral plots if len(xyz) == 3: fmt += ': %g' # x, y, z data for 2D image plot self.statusBar.showMessage(fmt % xyz) else: self.statusBar.clearMessage() def on_actionColormap_triggered(self): w = self.toolbar.widgetForAction(self.actionColormap) m = self.actionColormap.menu() if m: m.popup(w.mapToGlobal(QPoint(0, w.height()))) def on_colormap_triggered(self): action = self.actionsColormap.checkedAction() name = action.data() for widget in self._get_all_widgets(): widget.setColormap(COLORMAPS[name.upper()]) self.toolbar.widgetForAction(self.actionColormap).setText(name.title()) @pyqtSlot() def on_actionLines_triggered(self): if self.widget and isinstance(self.widget, LiveWidget1D): self.widget.setLines(self.actionLines.isChecked()) @pyqtSlot() def on_actionSymbols_triggered(self): if self.widget and isinstance(self.widget, LiveWidget1D): self.widget.setSymbols(self.actionSymbols.isChecked()) def _getLiveWidget(self, roi): return self._livewidgets.get(roi + '/roi', None) def showRoiWindow(self, roikey): key = roikey + '/roi' widget = self._getLiveWidget(roikey) region = self.widget._rois[key] if not widget: widget = LiveWidget(None) widget.setWindowTitle(roikey) widget.setColormap(self.widget.getColormap()) widget.setCenterMark(self.actionMarkCenter.isChecked()) widget.logscale(self.actionLogScale.isChecked()) widget.gr.setAdjustSelection(False) # don't use adjust on ROIs for name, roi in self.rois.items(): widget.setROI(name, roi) width = max(region.x) - min(region.x) height = max(region.y) - min(region.y) if width > height: dwidth = 500 dheight = 500 * height // width else: dheight = 500 dwidth = 500 * width // height widget.resize(dwidth, dheight) widget.closed.connect(self.on_roiWindowClosed) widget.setWindowForRoi(region) widget.update() widget.show() widget.activateWindow() self._livewidgets[key] = widget def closeRoiWindow(self, roi): widget = self._getLiveWidget(roi) if widget: widget.close() def on_closed(self): for w in self._livewidgets.values(): w.close() def _register_rois(self, detectors): self.rois.clear() self.actionROI.setVisible(False) self.menuROI = QMenu(self) self.actionsROI = QActionGroup(self) self.actionsROI.setExclusive(False) for detname in detectors: self.log.debug('checking rois for detector \'%s\'', detname) for tup in self.client.eval(detname + '.postprocess', ''): roi = tup[0] cachekey = roi + '/roi' # check whether or not this is a roi (cachekey exists). keyval = self.client.getCacheKey(cachekey) if keyval: self.on_roiChange(cachekey, keyval[1]) self.log.debug('register roi: %s', roi) # create roi menu action = self.menuROI.addAction(roi) action.setData(roi) action.setCheckable(True) self.actionsROI.addAction(action) action.triggered.connect(self.on_roi_triggered) self.actionROI.setMenu(self.menuROI) self.actionROI.setVisible(True) def on_actionROI_triggered(self): w = self.toolbar.widgetForAction(self.actionROI) self.actionROI.menu().popup(w.mapToGlobal(QPoint(0, w.height()))) def on_roi_triggered(self): action = self.sender() roi = action.data() if action.isChecked(): self.showRoiWindow(roi) else: self.closeRoiWindow(roi) def on_roiWindowClosed(self): widget = self.sender() if widget: key = None for key, w in self._livewidgets.items(): if w == widget: self.log.debug('delete roi: %s', key) del self._livewidgets[key] break if key: roi = key.rsplit('/', 1)[0] for action in self.actionsROI.actions(): if action.data() == roi: action.setChecked(False) self.log.debug('uncheck roi: %s', roi) def on_roiChange(self, key, value): self.log.debug('on_roiChange: %s %s', key, (value, )) self.rois[key] = value for widget in self._get_all_widgets(): widget.setROI(key, value) widget = self._livewidgets.get(key, None) if widget: widget.setWindowForRoi(self.widget._rois[key]) def on_cache(self, data): _time, key, _op, svalue = data try: value = cache_load(svalue) except ValueError: value = None if key in self.rois: self.on_roiChange(key, value) elif key == self.detectorskey and self.widget: self._register_rois(value) def on_client_connected(self): self.client.tell('eventunmask', ['livedata']) self.detectorskey = (self.client.eval('session.experiment.name') + '/detlist').lower() def normalizeType(self, dtype): normalized_type = numpy.dtype(dtype).str if normalized_type not in DATATYPES: self.log.warning('Unsupported live data format: %s', normalized_type) return return normalized_type def getIndexedUID(self, idx): return str(self.params['uid']) + '-' + str(idx) def _process_axis_labels(self, blobs): """Convert the raw axis label descriptions. tuple: `from, to`: Distribute labels equidistantly between the two values. numbertype: `index into labels`: Actual labels are provided. Value is the starting index. Extract from first available blob. Remove said blob from list. None: `default`: Start at 0 with stepwidth 1. Save the axis labels to the datacache. """ CLASSIC = {'define': 'classic'} for i, datadesc in enumerate(self.params['datadescs']): labels = {} titles = {} for size, axis in zip(reversed(datadesc['shape']), AXES): # if the 'labels' key does not exist or does not have the right # axis key set default to 'classic'. label = datadesc.get('labels', { 'x': CLASSIC, 'y': CLASSIC }).get(axis, CLASSIC) if label['define'] == 'range': start = label.get('start', 0) size = label.get('length', 1) step = label.get('step', 1) end = start + step * size labels[axis] = numpy.arange(start, end, step) elif label['define'] == 'array': index = label.get('index', 0) labels[axis] = numpy.frombuffer(blobs[index], label.get('dtype', '<i4')) else: labels[axis] = self.getDefaultLabels(size) labels[axis] += self._offset if axis == 'x' else 0 titles[axis] = label.get('title') # save the labels in the datacache with uid as key uid = self.getIndexedUID(i) if uid not in self._datacache: self._datacache[uid] = {} self._datacache[uid]['labels'] = labels self._datacache[uid]['titles'] = titles def _process_livedata(self, data, idx): # ignore irrelevant data in liveOnly mode if self._liveOnlyIndex is not None and idx != self._liveOnlyIndex: return try: descriptions = self.params['datadescs'] except KeyError: self.log.warning('Livedata with tag "Live" without ' '"datadescs" provided.') return # pylint: disable=len-as-condition if len(data): # we got live data with specified formats arrays = self.processDataArrays( idx, numpy.frombuffer(data, descriptions[idx]['dtype'])) if arrays is None: return # put everything into the cache uid = self.getIndexedUID(idx) self._datacache[uid]['dataarrays'] = arrays self.liveitems[idx].setData(FILEUID, uid) def _process_filenames(self): # TODO: allow multiple fileformats? # would need to modify input from DemonSession.notifyDataFile number_of_items = self.fileList.count() for i, filedesc in enumerate(self.params['filedescs']): uid = self.getIndexedUID(number_of_items + i) name = filedesc['filename'] filetype = filedesc.get('fileformat') if filetype is None or filetype not in ReaderRegistry.filetypes(): continue # Ignore unregistered file types self.add_to_flist(name, filetype, FILE, uid) try: # update display for selected live channel, # just cache otherwise self.setDataFromFile(name, filetype, uid, display=(i == self._livechannel)) except Exception as e: if uid in self._datacache: # image is already cached # suppress error message for cached image self.log.debug(e) else: # image is not cached and could not be loaded self.log.exception(e) def on_client_livedata(self, params, blobs): # blobs is a list of data blobs and labels blobs if self._allowed_detectors \ and params['det'] not in self._allowed_detectors: return self.params = params self._runtime = params['time'] if params['tag'] == LIVE: datacount = len(params['datadescs']) self.setLiveItems(datacount) self._process_axis_labels(blobs[datacount:]) for i, blob in enumerate(blobs[:datacount]): self._process_livedata(blob, i) if not datacount: self._process_livedata([], 0) elif params['tag'] == FILE: self._process_filenames() self._show() def getDefaultLabels(self, size): return numpy.array(range(size)) def convertLabels(self, labelinput): """Convert the input into a processable format""" for i, entry in enumerate(labelinput): if isinstance(entry, str): labelinput[i] = self.normalizeType(entry) return labelinput def _initLiveWidget(self, array): """Initialize livewidget based on array's shape""" if len(array.shape) == 1: widgetcls = LiveWidget1D else: widgetcls = IntegralLiveWidget self.initLiveWidget(widgetcls) def setDataFromFile(self, filename, filetype, uid=None, display=True): """Load data array from file and dispatch to live widgets using ``setData``. Do not use caching if uid is ``None``. """ array = readDataFromFile(filename, filetype) if array is not None: if uid: if uid not in self._datacache: self.log.debug('add to cache: %s', uid) self._datacache[uid] = {} self._datacache[uid]['dataarrays'] = [array] if display: self._initLiveWidget(array) for widget in self._get_all_widgets(): widget.setData(array) # self.setData([array], uid, display=display) return array.shape else: raise NicosError('Cannot read file %r' % filename) def processDataArrays(self, index, entry): """Check if the input 1D array has the expected amount of values. If the array is too small an Error is raised. If the size exceeds the expected amount it is truncated. Returns a list of arrays corresponding to the ``plotcount`` of ``index`` into ``datadescs`` of the current params""" datadesc = self.params['datadescs'][index] count = datadesc.get('plotcount', DEFAULTS['plotcount']) shape = datadesc['shape'] # ignore irrelevant data in liveOnly mode if self._liveOnlyIndex is not None and index != self._liveOnlyIndex: return # determine 1D array size arraysize = numpy.product(shape) # check and split the input array if len(entry) < count * arraysize: self.log.warning('Expected dataarray with %d entries, got %d', count * arraysize, len(entry)) return arrays = numpy.split(entry[:count * arraysize], count) # reshape every array in the list for i, array in enumerate(arrays): arrays[i] = array.reshape(shape) return arrays def applyPlotSettings(self): if not self.widget or not isinstance(self.widget, LiveWidget1D): return if self._liveOnlyIndex is not None: index = self._liveOnlyIndex elif self.fileList.currentItem() not in self.liveitems: return else: index = self.fileList.currentRow() if isinstance(self.widget, LiveWidget1D): def getElement(l, index, default): try: return l[index] except IndexError: return default settings = getElement(self.plotsettings, index, DEFAULTS) if self.params['tag'] == LIVE: plotcount = self.params['datadescs'][index].get( 'plotcount', DEFAULTS['plotcount']) else: plotcount = DEFAULTS['plotcount'] marks = [settings.get('marks', DEFAULTS['marks'])] markersize = settings.get('markersize', DEFAULTS['markersize']) offset = settings.get('offset', DEFAULTS['offset']) colors = settings.get('colors', DEFAULTS['colors']) if isinstance(colors, list): if len(colors) > plotcount: colors = colors[:plotcount] while len(colors) < plotcount: colors.append(DEFAULTS['colors']) else: colors = [colors] * plotcount self.setOffset(offset) self.widget.setMarks(marks) self.widget.setMarkerSize(markersize) self.widget.setPlotCount(plotcount, colors) def setOffset(self, offset): self._offset = offset def getDataFromItem(self, item): """Extract and return the data associated with the item. If the data is in the cache return it. If the data is in a valid file extract it from there. """ if item is None: return uid = item.data(FILEUID) # data is cached if uid and hasattr(self, '_datacache') and uid in self._datacache: return self._datacache[uid] # cache has cleared data or data has not been cached in the first place elif uid is None and item.data(FILETAG) == FILE: filename = item.data(FILENAME) filetype = item.data(FILETYPE) if path.isfile(filename): rawdata = readDataFromFile(filename, filetype) labels = {} titles = {} for axis, entry in zip(AXES, reversed(rawdata.shape)): labels[axis] = numpy.arange(entry) titles[axis] = axis data = { 'labels': labels, 'titles': titles, 'dataarrays': [rawdata] } return data # else: # TODO: mark for deletion on item changed? def _show(self, data=None): """Show the provided data. If no data has been provided extract it from the datacache via the current item's uid. :param data: dictionary containing 'dataarrays' and 'labels' """ idx = self.fileList.currentRow() if idx == -1: self.fileList.setCurrentRow(0) return # no data has been provided, try to get it from the cache if data is None: data = self.getDataFromItem(self.fileList.currentItem()) # still no data if data is None: return arrays = data.get('dataarrays', []) labels = data.get('labels', {}) titles = data.get('titles', {}) # if multiple datasets have to be displayed in one widget, they have # the same dimensions, so we only need the dimensions of one set self._initLiveWidget(arrays[0]) self.applyPlotSettings() for widget in self._get_all_widgets(): widget.setData(arrays, labels) widget.setTitles(titles) if self.unzoom and self.widget: self.on_actionUnzoom_triggered() def remove_obsolete_cached_files(self): """Remove or flag items which are no longer cached. The cache will delete items if it's size exceeds ´cachesize´. This checks the items in the filelist and their caching status, removing items with deleted associated files and flagging items with valid files to be reloaded if selected by the user. """ for index in reversed(range(self.fileList.count())): item = self.fileList.item(index) uid = item.data(FILEUID) # is the uid still cached if uid and uid not in self._datacache: # does the file still exist on the filesystem if path.isfile(item.data(FILENAME)): item.setData(FILEUID, None) else: self.fileList.takeItem(index) def add_to_flist(self, filename, filetype, tag, uid=None, scroll=True): # liveonly mode doesn't display a filelist if self._liveOnlyIndex is not None: return shortname = path.basename(filename) item = QListWidgetItem(shortname) item.setData(FILENAME, filename) item.setData(FILETYPE, filetype) item.setData(FILETAG, tag) item.setData(FILEUID, uid) self.fileList.insertItem(self.fileList.count(), item) if uid: self.remove_obsolete_cached_files() if scroll: self.fileList.scrollToBottom() return item def on_fileList_currentItemChanged(self): self._show() @pyqtSlot() def on_actionOpen_triggered(self): """Open image file using registered reader classes.""" ftypes = { ffilter: ftype for ftype, ffilter in ReaderRegistry.filefilters() if not self._allowed_filetypes or ftype in self._allowed_filetypes } fdialog = FileFilterDialog(self, "Open data files", "", ";;".join(ftypes.keys())) if self._fileopen_filter: fdialog.selectNameFilter(self._fileopen_filter) if fdialog.exec_() != fdialog.Accepted: return files = fdialog.selectedFiles() if not files: return self._fileopen_filter = fdialog.selectedNameFilter() filetype = ftypes[self._fileopen_filter] errors = [] def _cacheFile(fn, filetype): uid = uuid4() # setDataFromFile may raise an `NicosException`, e.g. # if the file cannot be opened. try: self.setDataFromFile(fn, filetype, uid, display=False) except Exception as err: errors.append('%s: %s' % (fn, err)) else: return self.add_to_flist(fn, filetype, FILE, uid) # load and display first item f = files.pop(0) item = _cacheFile(f, filetype) if item is not None: self.fileList.setCurrentItem(item) cachesize = self._cachesize - 1 # add first `cachesize` files to cache for _, f in enumerateWithProgress(files[:cachesize], "Loading data files...", parent=fdialog): _cacheFile(f, filetype) # add further files to file list (open on request/itemClicked) for f in files[cachesize:]: self.add_to_flist(f, filetype, FILE) if errors: self.showError('Some files could not be opened:\n\n' + '\n'.join(errors)) @pyqtSlot() def on_actionUnzoom_triggered(self): self.widget.unzoom() @pyqtSlot() def on_actionPrint_triggered(self): self.widget.printDialog() @pyqtSlot() def on_actionSavePlot_triggered(self): self.widget.savePlot() @pyqtSlot() def on_actionLogScale_triggered(self): for widget in self._get_all_widgets(): widget.logscale(self.actionLogScale.isChecked()) @pyqtSlot() def on_actionMarkCenter_triggered(self): flag = self.actionMarkCenter.isChecked() for widget in self._get_all_widgets(): widget.setCenterMark(flag) @pyqtSlot() def on_actionKeepRatio_triggered(self): self.widget.gr.setAdjustSelection(self.actionKeepRatio.isChecked())
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
class EditorPanel(Panel): """Provides a text editor specialized for entering scripts. Together with actions such as `Run` or `Simulate` it gives the user the opportunity to create and check measurement scripts. The editor widget uses `QScintilla` if it is installed, and a standard text edit box otherwise. Options: * ``tools`` (default None) -- a list of `tools` which may configure some special commands or scripts. The tools can generate code to insert into the editor window. The access to these tools will be given via a special menu ``Editor tools``. * ``show_browser`` (default True) -- Toggle the default visibility of the Script Browser widget. * ``sim_window`` -- how to display dry run results: either ``"inline"`` (in a dock widget in the panel, the default), ``"single"`` (in an external window, the same for each run), or ``"multi"`` (each run opens a new window). """ panelName = 'User editor' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/editor.ui') self.window = parent self.custom_font = None self.custom_back = None self.mainwindow.codeGenerated.connect(self.on_codeGenerated) if not has_scintilla: self.actionComment.setEnabled(False) self.menus = None self.bar = None self.current_status = None self.recentf_actions = [] self.searchdlg = None self.menuRecent = QMenu('Recent files') self.menuToolsActions = [] for fn in self.recentf: action = QAction(fn.replace('&', '&&'), self) action.setData(fn) action.triggered.connect(self.openRecentFile) self.recentf_actions.append(action) self.menuRecent.addAction(action) self.tabber = QTabWidget(self, tabsClosable=True, documentMode=True) self.tabber.currentChanged.connect(self.on_tabber_currentChanged) self.tabber.tabCloseRequested.connect(self.on_tabber_tabCloseRequested) self.toolconfig = options.get('tools') self.sim_window = options.get('sim_window', 'inline') if self.sim_window not in ('single', 'multi', 'inline'): self.log.warning('invalid sim_window option %r, using inline', self.sim_window) self.sim_window = 'inline' hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) hlayout.addWidget(self.tabber) self.mainFrame.setLayout(hlayout) self.editors = [] # tab index -> editor self.filenames = {} # editor -> filename self.watchers = {} # editor -> QFileSystemWatcher self.currentEditor = None self.saving = False # True while saving self.warnWidget.hide() self.simFrame = SimResultFrame(self, None, self.client) self.simPaneFrame.layout().addWidget(self.simFrame) self.simPane.hide() self.simWindows = [] self.splitter.restoreState(self.splitterstate) self.treeModel = QFileSystemModel() idx = self.treeModel.setRootPath('/') self.treeModel.setNameFilters(['*.py', '*.txt']) self.treeModel.setNameFilterDisables(False) # hide them self.fileTree.setModel(self.treeModel) self.fileTree.header().hideSection(1) self.fileTree.header().hideSection(2) self.fileTree.header().hideSection(3) self.fileTree.header().hide() self.fileTree.setRootIndex(idx) if not options.get('show_browser', True): self.scriptsPane.hide() self.actionShowScripts = self.scriptsPane.toggleViewAction() self.actionShowScripts.setText('Show Script Browser') self.activeGroup = QActionGroup(self) self.activeGroup.addAction(self.actionRun) self.activeGroup.addAction(self.actionSimulate) self.activeGroup.addAction(self.actionUpdate) client.simresult.connect(self.on_client_simresult) if self.client.connected: self.on_client_connected() else: self.on_client_disconnected() client.connected.connect(self.on_client_connected) client.disconnected.connect(self.on_client_disconnected) client.setup.connect(self.on_client_connected) client.cache.connect(self.on_client_cache) client.experiment.connect(self.on_client_experiment) if self.openfiles: for fn in self.openfiles: self.openFile(fn, quiet=True) else: self.newFile() def __del__(self): # On some systems the QFilesystemWatchers deadlock on application exit # so destroy them explicitly self.watchers.clear() def setViewOnly(self, viewonly): self.activeGroup.setEnabled(not viewonly) def getMenus(self): menuFile = QMenu('&File', self) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) menuFile.addAction(self.menuRecent.menuAction()) menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionPrint) menuView = QMenu('&View', self) menuView.addAction(self.actionShowScripts) menuEdit = QMenu('&Edit', self) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() menuEdit.addAction(self.actionComment) menuEdit.addSeparator() menuEdit.addAction(self.actionFind) menuScript = QMenu('&Script', self) menuScript.addSeparator() menuScript.addAction(self.actionRun) menuScript.addAction(self.actionSimulate) menuScript.addAction(self.actionUpdate) menuScript.addSeparator() menuScript.addAction(self.actionGet) if self.toolconfig: menuTools = QMenu('Editor t&ools', self) createToolMenu(self, self.toolconfig, menuTools) menus = [menuFile, menuView, menuEdit, menuScript, menuTools] else: menus = [menuFile, menuView, menuEdit, menuScript] self.menus = menus return self.menus def getToolbars(self): if not self.bar: bar = QToolBar('Editor') bar.addAction(self.actionNew) bar.addAction(self.actionOpen) bar.addAction(self.actionSave) bar.addSeparator() bar.addAction(self.actionPrint) bar.addSeparator() bar.addAction(self.actionUndo) bar.addAction(self.actionRedo) bar.addSeparator() bar.addAction(self.actionCut) bar.addAction(self.actionCopy) bar.addAction(self.actionPaste) bar.addSeparator() bar.addAction(self.actionRun) bar.addAction(self.actionSimulate) bar.addAction(self.actionGet) bar.addAction(self.actionUpdate) showToolText(bar, self.actionRun) showToolText(bar, self.actionSimulate) showToolText(bar, self.actionGet) showToolText(bar, self.actionUpdate) self.bar = bar return [self.bar] def updateStatus(self, status, exception=False): self.current_status = status def setCustomStyle(self, font, back): self.custom_font = font self.custom_back = back self.simFrame.simOutView.setFont(font) self.simFrame.simOutViewErrors.setFont(font) for editor in self.editors: self._updateStyle(editor) def _updateStyle(self, editor): if self.custom_font is None: return bold = QFont(self.custom_font) bold.setBold(True) if has_scintilla: lexer = editor.lexer() lexer.setDefaultFont(self.custom_font) for i in range(16): lexer.setFont(self.custom_font, i) # make keywords bold lexer.setFont(bold, 5) else: editor.setFont(self.custom_font) if has_scintilla: lexer.setPaper(self.custom_back) else: setBackgroundColor(editor, self.custom_back) def enableFileActions(self, on): for action in [ self.actionSave, self.actionSaveAs, self.actionReload, self.actionPrint, self.actionUndo, self.actionRedo, self.actionCut, self.actionCopy, self.actionPaste, self.actionFind, ]: action.setEnabled(on) self.enableExecuteActions(self.client.isconnected) for action in [self.actionComment]: action.setEnabled(on and has_scintilla) def enableExecuteActions(self, on): for action in [ self.actionRun, self.actionSimulate, self.actionGet, self.actionUpdate ]: action.setEnabled(self.client.isconnected) def on_codeGenerated(self, code): if self.currentEditor: self.currentEditor.beginUndoAction() if self.currentEditor.text(): res = OverwriteQuestion().exec_() if res == QMessageBox.Apply: self.currentEditor.clear() elif res == QMessageBox.Cancel: return # append() and setText() would clear undo history in QScintilla, # therefore we use these calls self.currentEditor.moveToEnd() self.currentEditor.insert(code) self.currentEditor.endUndoAction() else: self.showError('No script is opened at the moment.') def on_tabber_currentChanged(self, index): self.enableFileActions(index >= 0) if index == -1: self.currentEditor = None self.window.setWindowTitle('%s editor' % self.mainwindow.instrument) return editor = self.editors[index] fn = self.filenames[editor] if fn: self.window.setWindowTitle('%s[*] - %s editor' % (fn, self.mainwindow.instrument)) else: self.window.setWindowTitle('New[*] - %s editor' % self.mainwindow.instrument) self.window.setWindowModified(editor.isModified()) self.actionSave.setEnabled(editor.isModified()) self.actionUndo.setEnabled(editor.isModified()) self.currentEditor = editor if self.searchdlg: self.searchdlg.setEditor(editor) def on_tabber_tabCloseRequested(self, index): editor = self.editors[index] self._close(editor) def _close(self, editor): if not self.checkDirty(editor): return index = self.editors.index(editor) del self.editors[index] del self.filenames[editor] del self.watchers[editor] self.tabber.removeTab(index) def setDirty(self, editor, dirty): if editor is self.currentEditor: self.actionSave.setEnabled(dirty) self.actionUndo.setEnabled(dirty) self.window.setWindowModified(dirty) index = self.tabber.currentIndex() tt = self.tabber.tabText(index).rstrip('*') self.tabber.setTabText(index, tt + (dirty and '*' or '')) def loadSettings(self, settings): self.recentf = settings.value('recentf') or [] self.splitterstate = settings.value('splitter', '', QByteArray) self.openfiles = settings.value('openfiles') or [] def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('openfiles', [self.filenames[e] for e in self.editors if self.filenames[e]]) def requestClose(self): for editor in self.editors: if not self.checkDirty(editor): return False return True def createEditor(self): if has_scintilla: editor = QsciScintillaCustom(self) lexer = QsciLexerPython(editor) editor.setUtf8(True) editor.setLexer(lexer) editor.setAutoIndent(True) editor.setEolMode(QsciScintilla.EolUnix) editor.setIndentationsUseTabs(False) editor.setIndentationGuides(True) editor.setTabIndents(True) editor.setBackspaceUnindents(True) editor.setTabWidth(4) editor.setIndentationWidth(0) editor.setBraceMatching(QsciScintilla.SloppyBraceMatch) editor.setFolding(QsciScintilla.PlainFoldStyle) editor.setIndentationGuidesForegroundColor(QColor("#CCC")) editor.setWrapMode(QsciScintilla.WrapCharacter) editor.setMarginLineNumbers(1, True) editor.setMarginWidth( 1, 5 + 4 * QFontMetrics(editor.font()).averageCharWidth()) else: editor = QScintillaCompatible(self) # editor.setFrameStyle(0) editor.modificationChanged.connect( lambda dirty: self.setDirty(editor, dirty)) self._updateStyle(editor) return editor def on_client_connected(self): self.enableExecuteActions(True) self._set_scriptdir() def on_client_disconnected(self): self.enableExecuteActions(False) def _set_scriptdir(self): initialdir = self.client.eval('session.experiment.scriptpath', '') if initialdir: idx = self.treeModel.setRootPath(initialdir) self.fileTree.setRootIndex(idx) def on_client_cache(self, data): (_time, key, _op, _value) = data if key.endswith('/scriptpath'): self.on_client_connected() def on_client_simresult(self, data): if self.sim_window == 'inline': self.actionSimulate.setEnabled(True) def on_client_experiment(self, data): (_, proptype) = data self._set_scriptdir() self.simPane.hide() if proptype == 'user': # close existing tabs when switching TO a user experiment for index in range(len(self.editors) - 1, -1, -1): self.on_tabber_tabCloseRequested(index) # if all tabs have been closed, open a new file if not self.tabber.count(): self.on_actionNew_triggered() def on_fileTree_doubleClicked(self, idx): fpath = self.treeModel.filePath(idx) for i, editor in enumerate(self.editors): if self.filenames[editor] == fpath: self.tabber.setCurrentIndex(i) return self.openFile(fpath) @pyqtSlot() def on_actionPrint_triggered(self): if has_scintilla: printer = Printer() printer.setOutputFileName('') printer.setDocName(self.filenames[self.currentEditor]) # printer.setFullPage(True) if QPrintDialog(printer, self).exec_() == QDialog.Accepted: lexer = self.currentEditor.lexer() bgcolor = lexer.paper(0) # printer prints background color too, so set it to white lexer.setPaper(Qt.white) printer.printRange(self.currentEditor) lexer.setPaper(bgcolor) else: printer = QPrinter() printer.setOutputFileName('') if QPrintDialog(printer, self).exec_() == QDialog.Accepted: getattr(self.currentEditor, 'print')(printer) def validateScript(self): script = self.currentEditor.text() # XXX: this does not apply to .txt (SPM) scripts # try: # compile(script, 'script', 'exec') # except SyntaxError as err: # self.showError('Syntax error in script: %s' % err) # self.currentEditor.setCursorPosition(err.lineno - 1, err.offset) # return return script @pyqtSlot() def on_actionRun_triggered(self): script = self.validateScript() if script is None: return if not self.checkDirty(self.currentEditor, askonly=True): return if self.current_status != 'idle': if not self.askQuestion('A script is currently running, do you ' 'want to queue this script?', True): return self.client.run(script, self.filenames[self.currentEditor]) @pyqtSlot() def on_actionSimulate_triggered(self): script = self.validateScript() if script is None: return if not self.checkDirty(self.currentEditor, askonly=True): return simuuid = str(uuid1()) filename = self.filenames[self.currentEditor] if self.sim_window == 'inline': self.actionSimulate.setEnabled(False) self.simFrame.simuuid = simuuid self.simFrame.clear() self.simPane.setWindowTitle('Dry run results - %s' % filename) self.simPane.show() else: if self.sim_window == 'multi' or not self.simWindows: window = SimResultFrame(None, self, self.client) window.setWindowTitle('Dry run results - %s' % filename) window.layout().setContentsMargins(6, 6, 6, 6) window.simOutView.setFont(self.simFrame.simOutView.font()) window.simOutViewErrors.setFont(self.simFrame.simOutView.font()) window.show() self.simWindows.append(window) else: window = self.simWindows[0] window.clear() window.setWindowTitle('Dry run results - %s' % filename) window.activateWindow() window.simuuid = simuuid self.client.tell('simulate', filename, script, simuuid) @pyqtSlot() def on_actionUpdate_triggered(self): script = self.validateScript() if script is None: return if not self.checkDirty(self.currentEditor, askonly=True): return reason, ok = QInputDialog.getText( self, 'Update reason', 'For the logbook, you can enter a reason ' 'for the update here:', text='no reason specified') if not ok: return self.client.tell('update', script, reason) @pyqtSlot() def on_actionGet_triggered(self): script = self.client.ask('getscript') if script is not None: editor = self.newFile() editor.setText(script) def checkDirty(self, editor, askonly=False): if not editor.isModified(): return True if self.filenames[editor]: message = 'Save changes in %s before continuing?' % \ self.filenames[editor] else: message = 'Save new file before continuing?' buttons = QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel if askonly: buttons = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel rc = QMessageBox.question(self, 'User Editor', message, buttons) if rc in (QMessageBox.Save, QMessageBox.Yes): return self.saveFile(editor) if rc in (QMessageBox.Discard, QMessageBox.No): return True return False def on_fileSystemWatcher_fileChanged(self, filename): if self.saving: return editor = watcher = None for editor, watcher in self.watchers.items(): if watcher is self.sender(): break else: return if editor.isModified(): # warn the user self.warnText.setText( 'The file %r has changed on disk, but has also been edited' ' here.\nPlease use either File-Reload to load the' ' version on disk or File-Save to save this version.' % self.filenames[editor]) self.warnWidget.show() else: # reload without asking try: with open(self.filenames[editor]) as f: text = f.read() except Exception: return editor.setText(text) editor.setModified(False) # re-add the filename to the watcher if it was deleted # (happens for programs that do delete-write on save) if not watcher.files(): watcher.addPath(self.filenames[editor]) @pyqtSlot() def on_actionNew_triggered(self): self.newFile() def newFile(self): editor = self.createEditor() editor.setModified(False) self.editors.append(editor) self.filenames[editor] = '' self.watchers[editor] = QFileSystemWatcher(self) self.watchers[editor].fileChanged.connect( self.on_fileSystemWatcher_fileChanged) self.tabber.addTab(editor, '(New script)') self.tabber.setCurrentWidget(editor) self.simFrame.clear() editor.setFocus() return editor @pyqtSlot() def on_actionOpen_triggered(self): if self.currentEditor is not None and self.filenames[self.currentEditor]: initialdir = path.dirname(self.filenames[self.currentEditor]) else: initialdir = self.client.eval('session.experiment.scriptpath', '') fn = QFileDialog.getOpenFileName(self, 'Open script', initialdir, 'Script files (*.py *.txt)')[0] if not fn: return self.openFile(fn) self.addToRecentf(fn) @pyqtSlot() def on_actionReload_triggered(self): fn = self.filenames[self.currentEditor] if not fn: return if not self.checkDirty(self.currentEditor): return try: with open(fn, 'r') as f: text = f.read() except Exception as err: return self.showError('Opening file failed: %s' % err) self.currentEditor.setText(text) self.simFrame.clear() def openRecentFile(self): self.openFile(self.sender().data()) def openFile(self, fn, quiet=False): try: with open(fn.encode(sys.getfilesystemencoding())) as f: text = f.read() except Exception as err: if quiet: return return self.showError('Opening file failed: %s' % err) editor = self.createEditor() editor.setText(text) editor.setModified(False) # replace tab if it's a single new file if len(self.editors) == 1 and not self.filenames[self.editors[0]] and \ not self.editors[0].isModified(): self._close(self.editors[0]) self.editors.append(editor) self.filenames[editor] = fn self.watchers[editor] = QFileSystemWatcher(self) self.watchers[editor].fileChanged.connect( self.on_fileSystemWatcher_fileChanged) self.watchers[editor].addPath(fn) self.tabber.addTab(editor, path.basename(fn)) self.tabber.setCurrentWidget(editor) self.simFrame.clear() editor.setFocus() def addToRecentf(self, fn): new_action = QAction(fn.replace('&', '&&'), self) new_action.setData(fn) new_action.triggered.connect(self.openRecentFile) if self.recentf_actions: self.menuRecent.insertAction(self.recentf_actions[0], new_action) self.recentf_actions.insert(0, new_action) del self.recentf_actions[10:] else: self.menuRecent.addAction(new_action) self.recentf_actions.append(new_action) with self.sgroup as settings: settings.setValue('recentf', [a.data() for a in self.recentf_actions]) @pyqtSlot() def on_actionSave_triggered(self): self.saveFile(self.currentEditor) self.window.setWindowTitle( '%s[*] - %s editor' % (self.filenames[self.currentEditor], self.mainwindow.instrument)) @pyqtSlot() def on_actionSaveAs_triggered(self): self.saveFileAs(self.currentEditor) self.window.setWindowTitle( '%s[*] - %s editor' % (self.filenames[self.currentEditor], self.mainwindow.instrument)) def saveFile(self, editor): if not self.filenames[editor]: return self.saveFileAs(editor) try: self.saving = True try: with open(self.filenames[editor], 'w') as f: f.write(editor.text()) finally: self.saving = False except Exception as err: self.showError('Writing file failed: %s' % err) return False self.watchers[editor].addPath(self.filenames[editor]) editor.setModified(False) return True def saveFileAs(self, editor): if self.filenames[editor]: initialdir = path.dirname(self.filenames[editor]) else: initialdir = self.client.eval('session.experiment.scriptpath', '') if self.client.eval('session.spMode', False): defaultext = '.txt' flt = 'Script files (*.txt *.py)' else: defaultext = '.py' flt = 'Script files (*.py *.txt)' fn = QFileDialog.getSaveFileName(self, 'Save script', initialdir, flt)[0] if not fn: return False if not fn.endswith(('.py', '.txt')): fn += defaultext self.addToRecentf(fn) self.watchers[editor].removePath(self.filenames[editor]) self.filenames[editor] = fn self.tabber.setTabText(self.editors.index(editor), path.basename(fn)) return self.saveFile(editor) @pyqtSlot() def on_actionFind_triggered(self): if not self.searchdlg: self.searchdlg = SearchDialog(self, self.currentEditor, has_scintilla) self.searchdlg.setEditor(self.currentEditor) self.searchdlg.show() @pyqtSlot() def on_actionUndo_triggered(self): self.currentEditor.undo() @pyqtSlot() def on_actionRedo_triggered(self): self.currentEditor.redo() @pyqtSlot() def on_actionCut_triggered(self): self.currentEditor.cut() @pyqtSlot() def on_actionCopy_triggered(self): self.currentEditor.copy() @pyqtSlot() def on_actionPaste_triggered(self): self.currentEditor.paste() @pyqtSlot() def on_actionComment_triggered(self): clen = len(COMMENT_STR) # act on selection? if self.currentEditor.hasSelectedText(): # get the selection boundaries line1, index1, line2, index2 = self.currentEditor.getSelection() if index2 == 0: endLine = line2 - 1 else: endLine = line2 assert endLine >= line1 self.currentEditor.beginUndoAction() # iterate over the lines action = [] for line in range(line1, endLine + 1): if self.currentEditor.text(line).startswith(COMMENT_STR): self.currentEditor.setSelection(line, 0, line, clen) self.currentEditor.removeSelectedText() action.append(-1) else: self.currentEditor.insertAt(COMMENT_STR, line, 0) action.append(1) # adapt original selection boundaries if index1 > 0: if action[0] == 1: index1 += clen else: index1 = max(0, index1 - clen) if endLine > line1 and index2 > 0: if action[-1] == 1: index2 += clen else: index2 = max(0, index2 - clen) # restore selection accordingly self.currentEditor.setSelection(line1, index1, line2, index2) self.currentEditor.endUndoAction() else: # comment line line, _ = self.currentEditor.getCursorPosition() self.currentEditor.beginUndoAction() if self.currentEditor.text(line).startswith(COMMENT_STR): self.currentEditor.setSelection(line, 0, line, clen) self.currentEditor.removeSelectedText() else: self.currentEditor.insertAt(COMMENT_STR, line, 0) self.currentEditor.endUndoAction()
def getMenus(self): if not self.menus: menu1 = QMenu('&Data plot', self) menu1.addAction(self.actionSavePlot) menu1.addAction(self.actionPrint) menu1.addAction(self.actionAttachElog) menu1.addSeparator() menu1.addAction(self.actionResetPlot) menu1.addAction(self.actionAutoDisplay) menu1.addAction(self.actionCombine) menu1.addAction(self.actionClosePlot) menu1.addAction(self.actionDeletePlot) menu1.addSeparator() menu1.addAction(self.actionXAxis) menu1.addAction(self.actionYAxis) menu1.addAction(self.actionNormalized) menu1.addSeparator() menu1.addAction(self.actionUnzoom) menu1.addAction(self.actionLogXScale) menu1.addAction(self.actionLogScale) menu1.addAction(self.actionAutoScale) menu1.addAction(self.actionScaleX) menu1.addAction(self.actionScaleY) menu1.addAction(self.actionLegend) menu1.addAction(self.actionErrors) menu1.addSeparator() menu2 = QMenu('Data &manipulation', self) menu2.addAction(self.actionModifyData) menu2.addSeparator() ag = QActionGroup(menu2) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) menu2.addAction(self.actionFitPeak) menu2.addAction(self.actionPickInitial) menu2.addAction(self.actionFitPeakGaussian) menu2.addAction(self.actionFitPeakLorentzian) menu2.addAction(self.actionFitPeakPV) menu2.addAction(self.actionFitPeakPVII) menu2.addAction(self.actionFitTc) menu2.addAction(self.actionFitCosine) menu2.addAction(self.actionFitSigmoid) menu2.addAction(self.actionFitLinear) menu2.addAction(self.actionFitExponential) menu2.addSeparator() menu2.addAction(self.actionFitArby) self.menus = [menu1, menu2] return self.menus
def getMenus(self): menuFile = QMenu('&File', self) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) menuFile.addAction(self.menuRecent.menuAction()) menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionPrint) menuView = QMenu('&View', self) menuView.addAction(self.actionShowScripts) menuEdit = QMenu('&Edit', self) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() menuEdit.addAction(self.actionComment) menuEdit.addSeparator() menuEdit.addAction(self.actionFind) menuScript = QMenu('&Script', self) menuScript.addSeparator() menuScript.addAction(self.actionRun) menuScript.addAction(self.actionSimulate) menuScript.addAction(self.actionUpdate) menuScript.addSeparator() menuScript.addAction(self.actionGet) if self.toolconfig: menuTools = QMenu('Editor t&ools', self) createToolMenu(self, self.toolconfig, menuTools) menus = [menuFile, menuView, menuEdit, menuScript, menuTools] else: menus = [menuFile, menuView, menuEdit, menuScript] self.menus = menus return self.menus
class ScansPanel(Panel): """Provides a display for the scans of the current experiment. Options: * ``fit_functions`` (default {}) -- dictionary for special fitting functions. The name of the fit function is followed by a set of a list of fit parameters and the string containing the fit function code:: fit_functions = { 'Resonance': (['Vmax = 0.1', 'R = 0.6', 'f = ', 'L = ', 'C = '], 'Vmax / sqrt(R**2 + (f*L-1/(f*C))**2)'), } The function can use the all mathematical functions of the `numpy <http://www.numpy.org/>`_ package. """ panelName = 'Scans' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/scans.ui') ArbitraryFitter.arby_functions.update(options.get('fit_functions', {})) self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.layout().addWidget(self.statusBar) self.x_menu = QMenu(self) self.x_menu.aboutToShow.connect(self.on_x_menu_aboutToShow) self.actionXAxis.setMenu(self.x_menu) self.y_menu = QMenu(self) self.y_menu.aboutToShow.connect(self.on_y_menu_aboutToShow) self.actionYAxis.setMenu(self.y_menu) self.actionAutoDisplay.setChecked(True) self.norm_menu = QMenu(self) self.norm_menu.aboutToShow.connect(self.on_norm_menu_aboutToShow) self.actionNormalized.setMenu(self.norm_menu) quickfit = QShortcut(QKeySequence("G"), self) quickfit.activated.connect(self.on_quickfit) self.user_color = Qt.white self.user_font = QFont('Monospace') self.bulk_adding = False self.no_openset = False self.last_norm_selection = None self.fitclass = GaussFitter self.fitfuncmap = {} self.menus = None self.bars = None self.data = self.mainwindow.data # maps set uid -> plot self.setplots = {} # maps set uid -> list item self.setitems = {} # current plot object self.currentPlot = None # stack of set uids self.setUidStack = [] # uids of automatically combined datasets -> uid of combined one self.contSetUids = {} self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if self.tablecolwidth0 > 0: self.metaTable.setColumnWidth(0, self.tablecolwidth0) self.metaTable.setColumnWidth(1, self.tablecolwidth1) self.data.datasetAdded.connect(self.on_data_datasetAdded) self.data.pointsAdded.connect(self.on_data_pointsAdded) self.data.fitAdded.connect(self.on_data_fitAdded) client.experiment.connect(self.on_client_experiment) self.setCurrentDataset(None) self.updateList() def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) self.tablecolwidth0 = settings.value('tablecolwidth0', 0, int) self.tablecolwidth1 = settings.value('tablecolwidth1', 0, int) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('tablecolwidth0', self.metaTable.columnWidth(0)) settings.setValue('tablecolwidth1', self.metaTable.columnWidth(1)) def setCustomStyle(self, font, back): self.user_font = font self.user_color = back for plot in self.setplots.values(): plot.setBackgroundColor(back) plot.update() bold = QFont(font) bold.setBold(True) larger = scaledFont(font, 1.6) for plot in self.setplots.values(): plot.setFonts(font, bold, larger) def requestClose(self): # Always succeeds, but break up circular references so that the panel # object can be deleted properly. self.currentPlot = None self.setplots.clear() return True def _autoscale(self, x=None, y=None): xflag = x if x is not None else self.actionScaleX.isChecked() yflag = y if y is not None else self.actionScaleY.isChecked() if self.currentPlot: self.currentPlot.setAutoScaleFlags(xflag, yflag) self.actionAutoScale.setChecked(xflag or yflag) self.actionScaleX.setChecked(xflag) self.actionScaleY.setChecked(yflag) self.currentPlot.update() def enablePlotActions(self, on): for action in [ self.actionSavePlot, self.actionPrint, self.actionResetPlot, self.actionAttachElog, self.actionCombine, self.actionClosePlot, self.actionDeletePlot, self.actionLogXScale, self.actionLogScale, self.actionAutoScale, self.actionScaleX, self.actionScaleY, self.actionXAxis, self.actionYAxis, self.actionNormalized, self.actionUnzoom, self.actionLegend, self.actionModifyData, self.actionFitPeak, self.actionFitPeakPV, self.actionFitPeakPVII, self.actionFitTc, self.actionFitCosine, self.actionFitSigmoid, self.actionFitArby, self.actionErrors, ]: action.setEnabled(on) def enableAutoScaleActions(self, on): for action in [self.actionAutoScale, self.actionScaleX, self.actionScaleY]: action.setEnabled(on) def getMenus(self): if not self.menus: menu1 = QMenu('&Data plot', self) menu1.addAction(self.actionSavePlot) menu1.addAction(self.actionPrint) menu1.addAction(self.actionAttachElog) menu1.addSeparator() menu1.addAction(self.actionResetPlot) menu1.addAction(self.actionAutoDisplay) menu1.addAction(self.actionCombine) menu1.addAction(self.actionClosePlot) menu1.addAction(self.actionDeletePlot) menu1.addSeparator() menu1.addAction(self.actionXAxis) menu1.addAction(self.actionYAxis) menu1.addAction(self.actionNormalized) menu1.addSeparator() menu1.addAction(self.actionUnzoom) menu1.addAction(self.actionLogXScale) menu1.addAction(self.actionLogScale) menu1.addAction(self.actionAutoScale) menu1.addAction(self.actionScaleX) menu1.addAction(self.actionScaleY) menu1.addAction(self.actionLegend) menu1.addAction(self.actionErrors) menu1.addSeparator() menu2 = QMenu('Data &manipulation', self) menu2.addAction(self.actionModifyData) menu2.addSeparator() ag = QActionGroup(menu2) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) menu2.addAction(self.actionFitPeak) menu2.addAction(self.actionPickInitial) menu2.addAction(self.actionFitPeakGaussian) menu2.addAction(self.actionFitPeakLorentzian) menu2.addAction(self.actionFitPeakPV) menu2.addAction(self.actionFitPeakPVII) menu2.addAction(self.actionFitTc) menu2.addAction(self.actionFitCosine) menu2.addAction(self.actionFitSigmoid) menu2.addAction(self.actionFitLinear) menu2.addAction(self.actionFitExponential) menu2.addSeparator() menu2.addAction(self.actionFitArby) self.menus = [menu1, menu2] return self.menus def getToolbars(self): if not self.bars: bar = QToolBar('Scans') bar.addAction(self.actionSavePlot) bar.addAction(self.actionPrint) bar.addSeparator() bar.addAction(self.actionXAxis) bar.addAction(self.actionYAxis) bar.addAction(self.actionNormalized) bar.addSeparator() bar.addAction(self.actionLogXScale) bar.addAction(self.actionLogScale) bar.addAction(self.actionUnzoom) bar.addSeparator() bar.addAction(self.actionAutoScale) bar.addAction(self.actionScaleX) bar.addAction(self.actionScaleY) bar.addAction(self.actionLegend) bar.addAction(self.actionErrors) bar.addAction(self.actionResetPlot) bar.addAction(self.actionDeletePlot) bar.addSeparator() bar.addAction(self.actionAutoDisplay) bar.addAction(self.actionCombine) fitbar = QToolBar('Scan fitting') fitbar.addAction(self.actionFitPeak) wa = QWidgetAction(fitbar) self.fitPickCheckbox = QCheckBox(fitbar) self.fitPickCheckbox.setText('Pick') self.fitPickCheckbox.setChecked(True) self.actionPickInitial.setChecked(True) self.fitPickCheckbox.toggled.connect(self.actionPickInitial.setChecked) self.actionPickInitial.toggled.connect(self.fitPickCheckbox.setChecked) layout = QHBoxLayout() layout.setContentsMargins(10, 0, 10, 0) layout.addWidget(self.fitPickCheckbox) frame = QFrame(fitbar) frame.setLayout(layout) wa.setDefaultWidget(frame) fitbar.addAction(wa) ag = QActionGroup(fitbar) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) wa = QWidgetAction(fitbar) self.fitComboBox = QComboBox(fitbar) for a in ag.actions(): itemtext = a.text().replace('&', '') self.fitComboBox.addItem(itemtext) self.fitfuncmap[itemtext] = a self.fitComboBox.currentIndexChanged.connect( self.on_fitComboBox_currentIndexChanged) wa.setDefaultWidget(self.fitComboBox) fitbar.addAction(wa) fitbar.addSeparator() fitbar.addAction(self.actionFitArby) self.bars = [bar, fitbar] return self.bars def updateList(self): self.datasetList.clear() for dataset in self.data.sets: if dataset.invisible: continue shortname = '%s - %s' % (dataset.name, dataset.default_xname) item = QListWidgetItem(shortname, self.datasetList) item.setData(32, dataset.uid) self.setitems[dataset.uid] = item def on_logYinDomain(self, flag): if not flag: self.actionLogScale.setChecked(flag) def on_logXinDomain(self, flag): if not flag: self.actionLogXScale.setChecked(flag) def on_datasetList_currentItemChanged(self, item, previous): if self.no_openset or item is None: return self.openDataset(itemuid(item)) def on_datasetList_itemClicked(self, item): # this handler is needed in addition to currentItemChanged # since one can't change the current item if it's the only one if self.no_openset or item is None: return self.openDataset(itemuid(item)) def openDataset(self, uid): dataset = self.data.uid2set[uid] newplot = None if dataset.uid not in self.setplots: newplot = DataSetPlot(self.plotFrame, self, dataset) if self.currentPlot: newplot.enableCurvesFrom(self.currentPlot) self.setplots[dataset.uid] = newplot self.datasetList.setCurrentItem(self.setitems[uid]) plot = self.setplots[dataset.uid] self.enableAutoScaleActions(plot.HAS_AUTOSCALE) if newplot and plot.HAS_AUTOSCALE: from gr.pygr import PlotAxes plot.plot.autoscale = PlotAxes.SCALE_X | PlotAxes.SCALE_Y self.setCurrentDataset(plot) def setCurrentDataset(self, plot): if self.currentPlot: self.plotLayout.removeWidget(self.currentPlot) self.currentPlot.hide() self.metaTable.clearContents() self.currentPlot = plot if plot is None: self.enablePlotActions(False) else: try: self.setUidStack.remove(plot.dataset.uid) except ValueError: pass self.setUidStack.append(plot.dataset.uid) num_items = 0 for catname in INTERESTING_CATS: if catname in plot.dataset.headerinfo: num_items += 2 + len(plot.dataset.headerinfo[catname]) num_items -= 1 # remove last empty row self.metaTable.setRowCount(num_items) i = 0 for catname in INTERESTING_CATS: if catname in plot.dataset.headerinfo: values = plot.dataset.headerinfo[catname] catdesc = catname for name_desc in INFO_CATEGORIES: if name_desc[0] == catname: catdesc = name_desc[1] catitem = QTableWidgetItem(catdesc) font = catitem.font() font.setBold(True) catitem.setFont(font) self.metaTable.setItem(i, 0, catitem) self.metaTable.setSpan(i, 0, 1, 2) i += 1 for dev, name, value in sorted(values): key = '%s_%s' % (dev, name) if name != 'value' else dev self.metaTable.setItem(i, 0, QTableWidgetItem(key)) self.metaTable.setItem(i, 1, QTableWidgetItem(value)) if self.metaTable.columnSpan(i, 0) == 2: self.metaTable.setSpan(i, 0, 1, 1) i += 1 i += 1 self.metaTable.resizeRowsToContents() self.enablePlotActions(True) self.enableAutoScaleActions(self.currentPlot.HAS_AUTOSCALE) self.datasetList.setCurrentItem(self.setitems[plot.dataset.uid]) self.actionXAxis.setText('X axis: %s' % plot.current_xname) self.actionNormalized.setChecked(bool(plot.normalized)) self.actionLogScale.setChecked(plot.isLogScaling()) self.actionLogXScale.setChecked(plot.isLogXScaling()) self.actionLegend.setChecked(plot.isLegendEnabled()) self.actionErrors.setChecked(plot.isErrorBarEnabled()) if plot.HAS_AUTOSCALE: from gr.pygr import PlotAxes mask = plot.plot.autoscale self._autoscale(x=mask & PlotAxes.SCALE_X, y=mask & PlotAxes.SCALE_Y) plot.logYinDomain.connect(self.on_logYinDomain) plot.logXinDomain.connect(self.on_logXinDomain) self.plotLayout.addWidget(plot) plot.show() def on_data_datasetAdded(self, dataset): shortname = '%s - %s' % (dataset.name, dataset.default_xname) if dataset.uid in self.setitems: self.setitems[dataset.uid].setText(shortname) if dataset.uid in self.setplots: self.setplots[dataset.uid].updateDisplay() else: self.no_openset = True item = QListWidgetItem(shortname, self.datasetList) item.setData(32, dataset.uid) self.setitems[dataset.uid] = item if self.actionAutoDisplay.isChecked() and not self.data.bulk_adding: self.openDataset(dataset.uid) self.no_openset = False # If the dataset is a continuation of another dataset, automatically # create a combined dataset. contuids = dataset.continuation if contuids: alluids = tuple(contuids.split(',')) + (dataset.uid,) # Did we already create this set? Then don't create it again. if self.contSetUids.get(alluids) in self.setitems: return allsets = list(map(self.data.uid2set.get, alluids)) newuid = self._combine(COMBINE, allsets) if newuid: self.contSetUids[alluids] = newuid def on_data_pointsAdded(self, dataset): if dataset.uid in self.setplots: self.setplots[dataset.uid].pointsAdded() def on_data_fitAdded(self, dataset, res): if dataset.uid in self.setplots: self.setplots[dataset.uid]._plotFit(res) def on_client_experiment(self, data): self.datasetList.clear() # hide plot self.setCurrentDataset(None) # back to the beginning self.setplots = {} self.setitems = {} self.currentPlot = None self.setUidStack = [] @pyqtSlot() def on_actionClosePlot_triggered(self): current_set = self.setUidStack.pop() if self.setUidStack: self.setCurrentDataset(self.setplots[self.setUidStack[-1]]) else: self.setCurrentDataset(None) del self.setplots[current_set] @pyqtSlot() def on_actionResetPlot_triggered(self): current_set = self.setUidStack.pop() del self.setplots[current_set] self.openDataset(current_set) @pyqtSlot() def on_actionDeletePlot_triggered(self): if self.currentPlot.dataset.scaninfo != 'combined set': if not self.askQuestion('This is not a combined set: still ' 'delete it from the list?'): return current_set = self.setUidStack.pop() self.data.uid2set[current_set].invisible = True if self.setUidStack: self.setCurrentDataset(self.setplots[self.setUidStack[-1]]) else: self.setCurrentDataset(None) del self.setplots[current_set] for i in range(self.datasetList.count()): if itemuid(self.datasetList.item(i)) == current_set: self.datasetList.takeItem(i) break @pyqtSlot() def on_actionSavePlot_triggered(self): filename = self.currentPlot.savePlot() if filename: self.statusBar.showMessage('Plot successfully saved to %s.' % filename) @pyqtSlot() def on_actionPrint_triggered(self): if self.currentPlot.printPlot(): self.statusBar.showMessage('Plot successfully printed.') @pyqtSlot() def on_actionAttachElog_triggered(self): newdlg = dialogFromUi(self, 'panels/plot_attach.ui') suffix = self.currentPlot.SAVE_EXT newdlg.filename.setText( safeName('data_%s' % self.currentPlot.dataset.name + suffix)) ret = newdlg.exec_() if ret != QDialog.Accepted: return descr = newdlg.description.text() fname = newdlg.filename.text() pathname = self.currentPlot.saveQuietly() with open(pathname, 'rb') as fp: remotefn = self.client.ask('transfer', fp.read()) if remotefn is not None: self.client.eval('_LogAttach(%r, [%r], [%r])' % (descr, remotefn, fname)) os.unlink(pathname) @pyqtSlot() def on_actionUnzoom_triggered(self): self.currentPlot.unzoom() @pyqtSlot(bool) def on_actionLogScale_toggled(self, on): self.currentPlot.setLogScale(on) @pyqtSlot(bool) def on_actionLogXScale_toggled(self, on): self.currentPlot.setLogXScale(on) @pyqtSlot(bool) def on_actionAutoScale_toggled(self, on): self._autoscale(on, on) @pyqtSlot(bool) def on_actionScaleX_toggled(self, on): self._autoscale(x=on) @pyqtSlot(bool) def on_actionScaleY_toggled(self, on): self._autoscale(y=on) def on_x_menu_aboutToShow(self): self.x_menu.clear() if not self.currentPlot: return done = set() for name in self.currentPlot.dataset.xnameunits: if name in done: continue done.add(name) action = self.x_menu.addAction(name) action.setData(name) action.setCheckable(True) if name == self.currentPlot.current_xname: action.setChecked(True) action.triggered.connect(self.on_x_action_triggered) @pyqtSlot() def on_x_action_triggered(self, text=None): if text is None: text = self.sender().data() self.actionXAxis.setText('X axis: %s' % text) self.currentPlot.current_xname = text self.currentPlot.updateDisplay() self.on_actionUnzoom_triggered() @pyqtSlot() def on_actionXAxis_triggered(self): self.bars[0].widgetForAction(self.actionXAxis).showMenu() def on_y_menu_aboutToShow(self): self.y_menu.clear() if not self.currentPlot: return for curve in self.currentPlot.dataset.curves: action = self.y_menu.addAction(curve.full_description) action.setData(curve.full_description) action.setCheckable(True) if not curve.hidden: action.setChecked(True) action.triggered.connect(self.on_y_action_triggered) @pyqtSlot() def on_y_action_triggered(self, text=None): if text is None: text = self.sender().data() if not self.currentPlot: return for curve in self.currentPlot.dataset.curves: if curve.full_description == text: curve.hidden = not curve.hidden self.currentPlot.updateDisplay() self.on_actionUnzoom_triggered() @pyqtSlot() def on_actionYAxis_triggered(self): self.bars[0].widgetForAction(self.actionYAxis).showMenu() @pyqtSlot() def on_actionNormalized_triggered(self): if not self.currentPlot: return if self.currentPlot.normalized is not None: self.on_norm_action_triggered('None') else: all_normnames = [name for (_, name) in self.currentPlot.dataset.normindices] if self.last_norm_selection and \ self.last_norm_selection in all_normnames: use = self.last_norm_selection else: use = all_normnames[0] if all_normnames else 'None' self.on_norm_action_triggered(use) def on_norm_menu_aboutToShow(self): self.norm_menu.clear() if self.currentPlot: none_action = self.norm_menu.addAction('None') none_action.setData('None') none_action.setCheckable(True) none_action.setChecked(True) none_action.triggered.connect(self.on_norm_action_triggered) max_action = self.norm_menu.addAction('Maximum') max_action.setData('Maximum') max_action.setCheckable(True) if self.currentPlot.normalized == 'Maximum': max_action.setChecked(True) none_action.setChecked(False) max_action.triggered.connect(self.on_norm_action_triggered) for _, name in self.currentPlot.dataset.normindices: action = self.norm_menu.addAction(name) action.setData(name) action.setCheckable(True) if name == self.currentPlot.normalized: action.setChecked(True) none_action.setChecked(False) action.triggered.connect(self.on_norm_action_triggered) @pyqtSlot() def on_norm_action_triggered(self, text=None): if text is None: text = self.sender().data() if text == 'None': self.currentPlot.normalized = None self.actionNormalized.setChecked(False) else: self.last_norm_selection = text self.currentPlot.normalized = text self.actionNormalized.setChecked(True) self.currentPlot.updateDisplay() self.on_actionUnzoom_triggered() @pyqtSlot(bool) def on_actionLegend_toggled(self, on): self.currentPlot.setLegend(on) @pyqtSlot(bool) def on_actionErrors_toggled(self, on): self.currentPlot.setErrorBarEnabled(on) @pyqtSlot() def on_actionModifyData_triggered(self): self.currentPlot.modifyData() @pyqtSlot() def on_actionFitPeak_triggered(self): self.currentPlot.beginFit(self.fitclass, self.actionFitPeak, pickmode=self.fitPickCheckbox.isChecked()) @pyqtSlot(int) def on_fitComboBox_currentIndexChanged(self, index): self.fitfuncmap[self.fitComboBox.currentText()].trigger() @pyqtSlot() def on_actionFitPeakGaussian_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakGaussian.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = GaussFitter @pyqtSlot() def on_actionFitPeakLorentzian_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakLorentzian.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = LorentzFitter @pyqtSlot() def on_actionFitPeakPV_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakPV.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = PseudoVoigtFitter @pyqtSlot() def on_actionFitPeakPVII_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakPVII.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = PearsonVIIFitter @pyqtSlot() def on_actionFitTc_triggered(self): cbi = self.fitComboBox.findText(self.actionFitTc.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = TcFitter @pyqtSlot() def on_actionFitCosine_triggered(self): cbi = self.fitComboBox.findText(self.actionFitCosine.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = CosineFitter @pyqtSlot() def on_actionFitSigmoid_triggered(self): cbi = self.fitComboBox.findText(self.actionFitSigmoid.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = SigmoidFitter @pyqtSlot() def on_actionFitLinear_triggered(self): cbi = self.fitComboBox.findText(self.actionFitLinear.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = LinearFitter @pyqtSlot() def on_actionFitExponential_triggered(self): cbi = self.fitComboBox.findText(self.actionFitExponential.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = ExponentialFitter @pyqtSlot() def on_actionFitArby_triggered(self): # no second argument: the "arbitrary" action is not checkable self.currentPlot.beginFit(ArbitraryFitter, None, pickmode=self.fitPickCheckbox.isChecked()) @pyqtSlot() def on_quickfit(self): if not self.currentPlot or not self.currentPlot.underMouse(): return self.currentPlot.fitQuick() @pyqtSlot() def on_actionCombine_triggered(self): current = self.currentPlot.dataset.uid dlg = dialogFromUi(self, 'panels/dataops.ui') for i in range(self.datasetList.count()): item = self.datasetList.item(i) newitem = QListWidgetItem(item.text(), dlg.otherList) newitem.setData(32, item.data(32)) if itemuid(item) == current: dlg.otherList.setCurrentItem(newitem) # paint the current set in grey to indicate it's not allowed # to be selected newitem.setBackground(self.palette().brush(QPalette.Mid)) newitem.setFlags(Qt.NoItemFlags) if dlg.exec_() != QDialog.Accepted: return items = dlg.otherList.selectedItems() sets = [self.data.uid2set[current]] for item in items: if itemuid(item) == current: return self.showError('Cannot combine set with itself.') sets.append(self.data.uid2set[itemuid(item)]) for rop, rb in [(TOGETHER, dlg.opTogether), (COMBINE, dlg.opCombine), (ADD, dlg.opAdd), (SUBTRACT, dlg.opSubtract), (DIVIDE, dlg.opDivide)]: if rb.isChecked(): op = rop break self._combine(op, sets) def _combine(self, op, sets): if op == TOGETHER: newset = ScanData() newset.name = combineattr(sets, 'name', sep=', ') newset.invisible = False newset.curves = [] newset.scaninfo = 'combined set' # combine xnameunits from those that are in all sets all_xnu = set(sets[0].xnameunits) for dset in sets[1:]: all_xnu &= set(dset.xnameunits) newset.xnameunits = ['Default'] + [xnu for xnu in sets[0].xnameunits if xnu in all_xnu] newset.default_xname = 'Default' newset.normindices = sets[0].normindices # for together only, the number of curves and their columns # are irrelevant, just put all together for dataset in sets: for curve in dataset.curves: newcurve = curve.copy() if not newcurve.source: newcurve.source = dataset.name newset.curves.append(newcurve) self.data.add_existing_dataset(newset, [dataset.uid for dataset in sets]) return newset.uid # else, need same axes, and same number and types of curves firstset = sets[0] nameprops = [firstset.xnames, firstset.xunits] curveprops = [(curve.description, curve.yindex) for curve in firstset.curves] for dataset in sets[1:]: if [dataset.xnames, dataset.xunits] != nameprops: self.showError('Sets have different axes.') return if [(curve.description, curve.yindex) for curve in dataset.curves] != curveprops: self.showError('Sets have different curves.') return if op == COMBINE: newset = ScanData() newset.name = combineattr(sets, 'name', sep=', ') newset.invisible = False newset.curves = [] newset.scaninfo = 'combined set' newset.xnameunits = firstset.xnameunits newset.default_xname = firstset.default_xname newset.normindices = firstset.normindices for curves in zip(*(dataset.curves for dataset in sets)): newcurve = curves[0].copy() newcurve.datay = DataProxy(c.datay for c in curves) newcurve.datady = DataProxy(c.datady for c in curves) newcurve.datax = {xnu: DataProxy(c.datax[xnu] for c in curves) for xnu in newset.xnameunits} newcurve.datanorm = {nn: DataProxy(c.datanorm[nn] for c in curves) for i, nn in newset.normindices} newset.curves.append(newcurve) self.data.add_existing_dataset(newset, [dataset.uid for dataset in sets]) return newset.uid if op == ADD: sep = ' + ' elif op == SUBTRACT: sep = ' - ' elif op == DIVIDE: sep = ' / ' newset = ScanData() newset.name = combineattr(sets, 'name', sep=sep) newset.invisible = False newset.scaninfo = 'combined set' newset.curves = [] newset.xnameunits = firstset.xnameunits newset.default_xname = firstset.default_xname if op in (SUBTRACT, DIVIDE): # remove information about normalization -- doesn't make sense newset.normindices = [] else: newset.normindices = firstset.normindices for curves in zip(*(dataset.curves for dataset in sets)): newcurve = curves[0].deepcopy() # CRUDE HACK: don't care about the x values, operate by index removepoints = set() for curve in curves[1:]: for i in range(len(newcurve.datay)): y1, y2 = float(newcurve.datay[i]), float(curve.datay[i]) if newcurve.dyindex != -1: dy1 = newcurve.datady[i] dy2 = curve.datady[i] else: dy1 = dy2 = 1. if op == ADD: newcurve.datay[i] = y1 + y2 newcurve.datady[i] = sqrt(dy1**2 + dy2**2) for name in newcurve.datanorm: newcurve.datanorm[name][i] += curve.datanorm[name][i] elif op == SUBTRACT: newcurve.datay[i] = y1 - y2 newcurve.datady[i] = sqrt(dy1**2 + dy2**2) elif op == DIVIDE: if y2 == 0: y2 = 1. # generate a value for now removepoints.add(i) newcurve.datay[i] = y1 / y2 newcurve.datady[i] = sqrt((dy1/y2)**2 + (dy2*y1 / y2**2)**2) # remove points where we would have divided by zero if removepoints: newcurve.datay = [v for (i, v) in enumerate(newcurve.datay) if i not in removepoints] newcurve.datady = [v for (i, v) in enumerate(newcurve.datady) if i not in removepoints] for name in newcurve.datax: newcurve.datax[name] = \ [v for (i, v) in enumerate(newcurve.datax[name]) if i not in removepoints] for name in newcurve.datanorm: newcurve.datanorm[name] = \ [v for (i, v) in enumerate(newcurve.datanorm[name]) if i not in removepoints] newset.curves.append(newcurve) self.data.add_existing_dataset(newset) return newset.uid
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 getMenus(self): if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionOpen) menu.addAction(self.actionPrint) menu.addSeparator() menu.addAction(self.actionKeepRatio) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionColormap) menu.addAction(self.actionMarkCenter) menu.addAction(self.actionROI) self.menu = menu return [self.menu]
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/status.ui') self.stopcounting = False self.menus = None self.bar = None self.queueFrame.hide() self.statusLabel.hide() self.pause_color = QColor('#ffdddd') self.idle_color = parent.user_color self.script_queue = ScriptQueue(self.queueFrame, self.queueView) self.current_line = -1 self.current_request = {} self.curlineicon = QIcon(':/currentline') self.errlineicon = QIcon(':/errorline') empty = QPixmap(16, 16) empty.fill(Qt.transparent) self.otherlineicon = QIcon(empty) self.traceView.setItemDelegate(LineDelegate(24, self.traceView)) self.stopcounting = bool(options.get('stopcounting', False)) if self.stopcounting: tooltip = 'Aborts the current executed script' self.actionStop.setToolTip(tooltip) self.actionStop.setText('Abort current script') self.actionStop2.setToolTip(tooltip) self.showETA = bool(options.get('eta', False)) self.etaWidget.hide() client.request.connect(self.on_client_request) client.processing.connect(self.on_client_processing) client.blocked.connect(self.on_client_blocked) client.status.connect(self.on_client_status) client.initstatus.connect(self.on_client_initstatus) client.disconnected.connect(self.on_client_disconnected) client.rearranged.connect(self.on_client_rearranged) client.updated.connect(self.on_client_updated) client.eta.connect(self.on_client_eta) bar = QToolBar('Script control') bar.setObjectName(bar.windowTitle()) # unfortunately it is not wise to put a menu in its own dropdown menu, # so we have to duplicate the actionBreak and actionStop... dropdown1 = QMenu('', self) dropdown1.addAction(self.actionBreak) dropdown1.addAction(self.actionBreakCount) dropdown1.addAction(self.actionFinishEarly) self.actionBreak2.setMenu(dropdown1) dropdown2 = QMenu('', self) dropdown2.addAction(self.actionStop) dropdown2.addAction(self.actionFinish) dropdown2.addAction(self.actionFinishEarlyAndStop) self.actionStop2.setMenu(dropdown2) bar.addAction(self.actionBreak2) bar.addAction(self.actionContinue) bar.addAction(self.actionStop2) bar.addAction(self.actionEmergencyStop) self.bar = bar # self.mainwindow.addToolBar(bar) menu = QMenu('&Script control', self) menu.addAction(self.actionBreak) menu.addAction(self.actionBreakCount) menu.addAction(self.actionContinue) menu.addAction(self.actionFinishEarly) menu.addSeparator() menu.addAction(self.actionStop) menu.addAction(self.actionFinish) menu.addAction(self.actionFinishEarlyAndStop) menu.addSeparator() menu.addAction(self.actionEmergencyStop) self.mainwindow.menuBar().insertMenu( self.mainwindow.menuWindows.menuAction(), menu) self.activeGroup = QActionGroup(self) self.activeGroup.addAction(self.actionBreak) self.activeGroup.addAction(self.actionBreak2) self.activeGroup.addAction(self.actionBreakCount) self.activeGroup.addAction(self.actionContinue) self.activeGroup.addAction(self.actionStop) self.activeGroup.addAction(self.actionStop2) self.activeGroup.addAction(self.actionFinish) self.activeGroup.addAction(self.actionFinishEarly) self.activeGroup.addAction(self.actionFinishEarlyAndStop) self._status = 'idle'
class LiveDataPanel(Panel): """Provides a generic "detector live view". For most instruments, a specific panel must be implemented that takes care of the individual live display needs. Options: * ``instrument`` -- the instrument name that is passed on to the livewidget module. * ``filetypes`` default[] - List of filename extensions whose content should be displayed. * ``detectors`` (default [] - list of detector devices whose data should be displayed. If not set data from all configured detectors will be shown. * ``cachesize`` (default 20) - Number of entries in the live data cache. The live data cache allows to display of previous taken data. """ panelName = 'Live data view' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/live.ui') self._allowed_tags = set() self._allowed_detectors = set() self._ignore_livedata = False # ignore livedata, e.g. wrong detector self._last_idx = 0 self._last_tag = None self._last_fnames = None self._last_format = None self._runtime = 0 self._range_active = False self._cachesize = 20 self._livewidgets = {} # livewidgets for rois: roi_key -> widget self._fileopen_filter = None self.widget = None self.menu = None self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.toolbar = QToolBar('Live data') self.toolbar.addAction(self.actionOpen) self.toolbar.addAction(self.actionPrint) self.toolbar.addSeparator() self.toolbar.addAction(self.actionLogScale) self.toolbar.addSeparator() self.toolbar.addAction(self.actionKeepRatio) self.toolbar.addAction(self.actionUnzoom) self.toolbar.addAction(self.actionColormap) self.toolbar.addAction(self.actionMarkCenter) self.toolbar.addAction(self.actionROI) self._actions2D = [self.actionROI, self.actionColormap] self.setControlsEnabled(False) self.set2DControlsEnabled(False) # self.widget.setControls(Logscale | MinimumMaximum | BrightnessContrast | # Integrate | Histogram) self.liveitems = [] self.setLiveItems(1) self._livechannel = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if hasattr(self.window(), 'closed'): self.window().closed.connect(self.on_closed) client.livedata.connect(self.on_client_livedata) client.liveparams.connect(self.on_client_liveparams) client.connected.connect(self.on_client_connected) client.cache.connect(self.on_cache) self.rois = {} self.detectorskey = None # configure instrument specific behavior self._instrument = options.get('instrument', '') # self.widget.setInstrumentOption(self._instrument) # if self._instrument == 'toftof': # self.widget.setAxisLabels('time channels', 'detectors') # elif self._instrument == 'imaging': # self.widget.setControls(ShowGrid | Logscale | Grayscale | # Normalize | Darkfield | Despeckle | # CreateProfile | Histogram | MinimumMaximum) # self.widget.setStandardColorMap(True, False) # configure allowed file types supported_filetypes = ReaderRegistry.filetypes() opt_filetypes = set(options.get('filetypes', supported_filetypes)) self._allowed_tags = opt_filetypes & set(supported_filetypes) # configure allowed detector device names detectors = options.get('detectors') if detectors: self._allowed_detectors = set(detectors) # configure caching self._cachesize = options.get('cachesize', self._cachesize) if self._cachesize < 1: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize) def setLiveItems(self, n): nitems = len(self.liveitems) if n < nitems: nfiles = self.fileList.count() for i in range(nitems - 1, n - 1, -1): self.liveitems.pop(i) self.fileList.takeItem(nfiles - nitems + i) if self._livechannel > n: self._livechannel = 0 if n > 0 else None else: for i in range(nitems, n): item = QListWidgetItem('<Live #%d>' % (i + 1)) item.setData(FILENAME, i) item.setData(FILEFORMAT, '') item.setData(FILETAG, 'live') self.fileList.insertItem(self.fileList.count(), item) self.liveitems.append(item) if n == 1: self.liveitems[0].setText('<Live>') else: self.liveitems[0].setText('<Live #1>') def set2DControlsEnabled(self, flag): if flag != self.actionKeepRatio.isChecked(): self.actionKeepRatio.trigger() for action in self._actions2D: action.setVisible(flag) def setControlsEnabled(self, flag): for action in self.toolbar.actions(): action.setEnabled(flag) self.actionOpen.setEnabled(True) # File Open action always available def initLiveWidget(self, widgetcls): if isinstance(self.widget, widgetcls): return if self.widget: self.widgetLayout.removeWidget(self.widget) self.widget.deleteLater() self.widget = widgetcls(self) # enable/disable controls and set defaults for new livewidget instances self.setControlsEnabled(True) if isinstance(self.widget, LiveWidget1D): self.set2DControlsEnabled(False) else: self.set2DControlsEnabled(True) # apply current settings self.widget.setCenterMark(self.actionMarkCenter.isChecked()) self.widget.logscale(self.actionLogScale.isChecked()) guiConn = GUIConnector(self.widget.gr) guiConn.connect(MouseEvent.MOUSE_MOVE, self.on_mousemove_gr) self.menuColormap = QMenu(self) self.actionsColormap = QActionGroup(self) activeMap = self.widget.getColormap() activeCaption = None for name, value in iteritems(COLORMAPS): caption = name.title() action = self.menuColormap.addAction(caption) action.setData(caption) action.setCheckable(True) if activeMap == value: action.setChecked(True) # update toolButton text later otherwise this may fail # depending on the setup and qt versions in use activeCaption = caption self.actionsColormap.addAction(action) action.triggered.connect(self.on_colormap_triggered) self.actionColormap.setMenu(self.menuColormap) self.widgetLayout.addWidget(self.widget) if activeCaption: self.toolbar.widgetForAction( self.actionColormap).setText(activeCaption) detectors = self.client.eval('session.experiment.detectors', []) self._register_rois(detectors) def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('geometry', self.saveGeometry()) def getMenus(self): if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionOpen) menu.addAction(self.actionPrint) menu.addSeparator() menu.addAction(self.actionKeepRatio) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionColormap) menu.addAction(self.actionMarkCenter) menu.addAction(self.actionROI) self.menu = menu return [self.menu] def _get_all_widgets(self): yield self.widget for w in itervalues(self._livewidgets): yield w def getToolbars(self): return [self.toolbar] def on_mousemove_gr(self, event): xyz = None if event.getWindow(): # inside plot xyz = self.widget.getZValue(event) if xyz: fmt = '(%g, %g)' # x, y data 1D integral plots if len(xyz) == 3: fmt += ': %g' # x, y, z data for 2D image plot self.statusBar.showMessage(fmt % xyz) else: self.statusBar.clearMessage() def on_actionColormap_triggered(self): w = self.toolbar.widgetForAction(self.actionColormap) m = self.actionColormap.menu() if m: m.popup(w.mapToGlobal(QPoint(0, w.height()))) def on_colormap_triggered(self): action = self.actionsColormap.checkedAction() name = action.data() for widget in self._get_all_widgets(): widget.setColormap(COLORMAPS[name.upper()]) self.toolbar.widgetForAction(self.actionColormap).setText(name.title()) def _getLiveWidget(self, roi): return self._livewidgets.get(roi + '/roi', None) def showRoiWindow(self, roikey): key = roikey + '/roi' widget = self._getLiveWidget(roikey) region = self.widget._rois[key] if not widget: widget = LiveWidget(None) widget.setWindowTitle(roikey) widget.setColormap(self.widget.getColormap()) widget.setCenterMark(self.actionMarkCenter.isChecked()) widget.logscale(self.actionLogScale.isChecked()) widget.gr.setAdjustSelection(False) # don't use adjust on ROIs for name, roi in iteritems(self.rois): widget.setROI(name, roi) width = max(region.x) - min(region.x) height = max(region.y) - min(region.y) if width > height: dwidth = 500 dheight = 500 * height // width else: dheight = 500 dwidth = 500 * width // height widget.resize(dwidth, dheight) widget.closed.connect(self.on_roiWindowClosed) widget.setWindowForRoi(region) widget.update() widget.show() widget.activateWindow() self._livewidgets[key] = widget def closeRoiWindow(self, roi): widget = self._getLiveWidget(roi) if widget: widget.close() def on_closed(self): for w in self._livewidgets.values(): w.close() def _register_rois(self, detectors): self.rois.clear() self.actionROI.setVisible(False) self.menuROI = QMenu(self) self.actionsROI = QActionGroup(self) self.actionsROI.setExclusive(False) for detname in detectors: self.log.debug('checking rois for detector \'%s\'', detname) for tup in self.client.eval(detname + '.postprocess', ''): roi = tup[0] cachekey = roi + '/roi' # check whether or not this is a roi (cachekey exists). keyval = self.client.getCacheKey(cachekey) if keyval: self.on_roiChange(cachekey, keyval[1]) self.log.debug('register roi: %s', roi) # create roi menu action = self.menuROI.addAction(roi) action.setData(roi) action.setCheckable(True) self.actionsROI.addAction(action) action.triggered.connect(self.on_roi_triggered) self.actionROI.setMenu(self.menuROI) self.actionROI.setVisible(True) def on_actionROI_triggered(self): w = self.toolbar.widgetForAction(self.actionROI) self.actionROI.menu().popup(w.mapToGlobal(QPoint(0, w.height()))) def on_roi_triggered(self): action = self.sender() roi = action.data() if action.isChecked(): self.showRoiWindow(roi) else: self.closeRoiWindow(roi) def on_roiWindowClosed(self): widget = self.sender() if widget: key = None for key, w in iteritems(self._livewidgets): if w == widget: self.log.debug('delete roi: %s', key) del self._livewidgets[key] break if key: roi = key.rsplit('/', 1)[0] for action in self.actionsROI.actions(): if action.data() == roi: action.setChecked(False) self.log.debug('uncheck roi: %s', roi) def on_roiChange(self, key, value): self.log.debug('on_roiChange: %s %s', key, (value, )) self.rois[key] = value for widget in self._get_all_widgets(): widget.setROI(key, value) widget = self._livewidgets.get(key, None) if widget: widget.setWindowForRoi(self.widget._rois[key]) def on_cache(self, data): _time, key, _op, svalue = data try: value = cache_load(svalue) except ValueError: value = None if key in self.rois: self.on_roiChange(key, value) elif key == self.detectorskey and self.widget: self._register_rois(value) def on_client_connected(self): self.client.tell('eventunmask', ['livedata', 'liveparams']) datapath = self.client.eval('session.experiment.datapath', '') if not datapath or not path.isdir(datapath): return if self._instrument == 'imaging': for fn in sorted(os.listdir(datapath)): if fn.endswith('.fits'): self.add_to_flist(path.join(datapath, fn), '', 'fits', False) self.detectorskey = (self.client.eval('session.experiment.name') + '/detlist').lower() def on_client_liveparams(self, params): tag, uid, det, fname, dtype, nx, ny, nz, runtime = params # TODO: remove compatibility code if isinstance(fname, string_types): fname, nx, ny, nz = [fname], [nx], [ny], [nz] if self._allowed_detectors and det not in self._allowed_detectors: self._ignore_livedata = True return self._ignore_livedata = False self._runtime = runtime self._last_uid = uid if dtype: self.setLiveItems(len(fname)) self._last_fnames = None normalized_type = numpy.dtype(dtype).str if normalized_type not in DATATYPES: self._last_format = None self.log.warning('Unsupported live data format: %s', (params, )) return self._last_format = normalized_type elif fname: self._last_fnames = fname self._last_format = None self._last_tag = tag.lower() self._nx = nx self._ny = ny self._nz = nz self._last_idx = 0 def _initLiveWidget(self, array): """Initialize livewidget based on array's shape""" if len(array.shape) == 1: widgetcls = LiveWidget1D else: widgetcls = IntegralLiveWidget self.initLiveWidget(widgetcls) def setData(self, array, uid=None, display=True): """Dispatch data array to corresponding live widgets. Cache array based on uid parameter. No caching if uid is ``None``. """ if uid: if uid not in self._datacache: self.log.debug('add to cache: %s', uid) self._datacache[uid] = array if display: self._initLiveWidget(array) for widget in self._get_all_widgets(): widget.setData(array) def setDataFromFile(self, filename, tag, uid=None, display=True): """Load data array from file and dispatch to live widgets using ``setData``. Do not use caching if uid is ``None``. """ try: array = ReaderRegistry.getReaderCls(tag).fromfile(filename) except KeyError: raise NicosError('Unsupported fileformat %r' % tag) if array is not None: self.setData(array, uid, display=display) else: raise NicosError('Cannot read file %r' % filename) def on_client_livedata(self, data): if self._ignore_livedata: # ignore all live events return idx = self._last_idx # 0 <= array number < n self._last_idx += 1 # check for allowed tags but always allow live data if self._last_tag in self._allowed_tags or self._last_tag == 'live': # pylint: disable=len-as-condition if len(data) and self._last_format: # we got live data with a specified format uid = str(self._last_uid) + '-' + str(idx) array = numpy.frombuffer(data, self._last_format) if self._nz[idx] > 1: array = array.reshape( (self._nz[idx], self._ny[idx], self._nx[idx])) elif self._ny[idx] > 1: array = array.reshape((self._ny[idx], self._nx[idx])) # update display for selected live channel, just cache # otherwise self.setData(array, uid, display=(idx == self._livechannel)) self.liveitems[idx].setData(FILEUID, uid) else: # we got no live data, but a filename with the data # filename corresponds to full qualififed path here for i, filename in enumerate(self._last_fnames): uid = str(self._last_uid) + '-' + str(i) self.add_to_flist(filename, self._last_format, self._last_tag, uid) try: # update display for selected live channel, just cache # otherwise self.setDataFromFile(filename, self._last_tag, uid, display=(i == self._livechannel)) except Exception as e: if uid in self._datacache: # image is already cached # suppress error message for cached image self.log.debug(e) else: # image is not cached and could not be loaded self.log.exception(e) def remove_obsolete_cached_files(self): """Removes outdated cached files from the file list or set cached flag to False if the file is still available on the filesystem. """ cached_item_rows = [] for row in range(self.fileList.count()): item = self.fileList.item(row) if item.data(FILEUID): cached_item_rows.append(row) if len(cached_item_rows) > self._cachesize: for row in cached_item_rows[0:-self._cachesize]: item = self.fileList.item(row) self.log.debug('remove from cache %s %s', item.data(FILEUID), item.data(FILENAME)) if path.isfile(item.data(FILENAME)): item.setData(FILEUID, None) else: self.fileList.takeItem(row) def add_to_flist(self, filename, fformat, ftag, uid=None, scroll=True): shortname = path.basename(filename) item = QListWidgetItem(shortname) item.setData(FILENAME, filename) item.setData(FILEFORMAT, fformat) item.setData(FILETAG, ftag) item.setData(FILEUID, uid) self.fileList.insertItem(self.fileList.count() - len(self.liveitems), item) if uid: self.remove_obsolete_cached_files() if scroll: self.fileList.scrollToBottom() return item def on_fileList_itemClicked(self, item): if item is None: return fname = item.data(FILENAME) ftag = item.data(FILETAG) if item in self.liveitems and ftag == 'live': # show live image self._livechannel = int(fname) fname = None self.log.debug("set livechannel: %d", self._livechannel) else: self._livechannel = None self.log.debug("no direct display") uid = item.data(FILEUID) if uid: # show image from cache array = self._datacache.get(uid, None) if array is not None and array.size: self.setData(array) return if fname: # show image from file self.setDataFromFile(fname, ftag) def on_fileList_currentItemChanged(self, item, previous): self.on_fileList_itemClicked(item) @pyqtSlot() def on_actionOpen_triggered(self): """Open image file using registered reader classes.""" ftypes = { ffilter: ftype for ftype, ffilter in ReaderRegistry.filefilters() } fdialog = FileFilterDialog(self, "Open data files", "", ";;".join(ftypes.keys())) if self._fileopen_filter: fdialog.selectNameFilter(self._fileopen_filter) if fdialog.exec_() == fdialog.Accepted: self._fileopen_filter = fdialog.selectedNameFilter() tag = ftypes[self._fileopen_filter] files = fdialog.selectedFiles() if files: def _cacheFile(fn, tag): uid = uuid4() # setDataFromFile may raise an `NicosException`, e.g. # if the file cannot be opened. self.setDataFromFile(fn, tag, uid, display=False) return self.add_to_flist(fn, None, tag, uid) # load and display first item f = files.pop(0) self.fileList.setCurrentItem(_cacheFile(f, tag)) cachesize = self._cachesize - 1 # add first `cachesize` files to cache for _, f in enumerateWithProgress(files[:cachesize], "Loading data files...", parent=fdialog): _cacheFile(f, tag) # add further files to file list (open on request/itemClicked) for f in files[cachesize:]: self.add_to_flist(f, None, tag) @pyqtSlot() def on_actionUnzoom_triggered(self): self.widget.unzoom() @pyqtSlot() def on_actionPrint_triggered(self): self.widget.printDialog() @pyqtSlot() def on_actionLogScale_triggered(self): for widget in self._get_all_widgets(): widget.logscale(self.actionLogScale.isChecked()) @pyqtSlot() def on_actionMarkCenter_triggered(self): flag = self.actionMarkCenter.isChecked() for widget in self._get_all_widgets(): widget.setCenterMark(flag) @pyqtSlot() def on_actionKeepRatio_triggered(self): self.widget.gr.setAdjustSelection(self.actionKeepRatio.isChecked())
class ConsolePanel(Panel): """Provides a console-like interface. The commands can be entered and the output from the NICOS daemon is displayed. Options: * ``hasinput`` (default True) -- if set to False, the input box is hidden and the console is just an output view. * ``hasmenu`` (default True) -- if set to False, the console does not provide its menu (containing actions for the output view such as Save or Print). * ``fulltime`` (default False) -- if set to True, the console shows the full (date + time) timestamp for every line, instead of only for errors and warnings. * ``watermark`` (default empty) -- the path to an image file that should be used as a watermark in the console window. """ panelName = 'Console' ui = 'panels/console.ui' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, self.ui) self.commandInput.scrollWidget = self.outView self.grepPanel.hide() self.grepText.scrollWidget = self.outView self.actionLabel.hide() self.outView.setActionLabel(self.actionLabel) self.commandInput.history = self.cmdhistory self.commandInput.completion_callback = self.completeInput self.grepNoMatch.setVisible(False) self.actionAllowLineWrap.setChecked(self.mainwindow.allowoutputlinewrap) client.connected.connect(self.on_client_connected) client.message.connect(self.on_client_message) client.simmessage.connect(self.on_client_simmessage) client.initstatus.connect(self.on_client_initstatus) client.mode.connect(self.on_client_mode) client.experiment.connect(self.on_client_experiment) self.outView.setContextMenuPolicy(Qt.CustomContextMenu) self.menu = QMenu('&Output', self) self.menu.addAction(self.actionCopy) self.menu.addAction(self.actionGrep) self.menu.addSeparator() self.menu.addAction(self.actionSave) self.menu.addAction(self.actionPrint) self.menu.addSeparator() self.menu.addAction(self.actionAllowLineWrap) self.on_actionAllowLineWrap_triggered( self.mainwindow.allowoutputlinewrap) self.hasinput = bool(options.get('hasinput', True)) self.inputFrame.setVisible(self.hasinput) self.hasmenu = bool(options.get('hasmenu', True)) if options.get('fulltime', False): self.outView.setFullTimestamps(True) watermark = options.get('watermark', '') if watermark: watermark = findResource(watermark) if path.isfile(watermark): self.outView.setBackgroundImage(watermark) def on_outView_customContextMenuRequested(self, point): self.menu.popup(self.outView.mapToGlobal(point)) def setExpertMode(self, expert): if not self.hasinput: self.inputFrame.setVisible(expert) def setViewOnly(self, viewonly): self.commandInput.setVisible(not viewonly) self.promptLabel.setVisible(not viewonly) def loadSettings(self, settings): self.cmdhistory = settings.value('cmdhistory') or [] def saveSettings(self, settings): # only save 100 entries of the history cmdhistory = self.commandInput.history[-100:] settings.setValue('cmdhistory', cmdhistory) def getMenus(self): if self.hasmenu: return [self.menu] return [] def setCustomStyle(self, font, back): self.commandInput.idle_color = back for widget in (self.outView, self.commandInput): widget.setFont(font) setBackgroundColor(widget, back) self.promptLabel.setFont(font) def updateStatus(self, status, exception=False): self.commandInput.setStatus(status) def completeInput(self, fullstring, lastword): try: return self.client.ask('complete', fullstring, lastword, default=[]) except Exception: return [] def on_client_connected(self): self.actionLabel.hide() self.outView._currentuser = self.client.login def on_client_mode(self, mode): self.promptLabel.setText(modePrompt(mode)) def on_client_initstatus(self, state): self.on_client_mode(state['mode']) self.outView.clear() messages = self.client.ask('getmessages', '10000', default=[]) total = len(messages) // 2500 + 1 for _, batch in enumerateWithProgress(chunks(messages, 2500), text='Synchronizing...', parent=self, total=total): self.outView.addMessages(batch) self.outView.scrollToBottom() def on_client_message(self, message): self.outView.addMessage(message) def on_client_simmessage(self, simmessage): if simmessage[-1] == '0': self.outView.addMessage(simmessage) def on_client_experiment(self, data): (_, proptype) = data if proptype == 'user': # only clear history and output when switching TO a user experiment self.commandInput.history = [] # clear everything except the last command with output self.outView.clearAlmostEverything() def on_outView_anchorClicked(self, url): """Called when the user clicks a link in the out view.""" scheme = url.scheme() if scheme == 'exec': # Direct execution is too dangerous. Just insert it in the editor. if self.inputFrame.isVisible(): self.commandInput.setText(url.path()) self.commandInput.setFocus() elif scheme == 'edit': if self.mainwindow.editor_wintype is None: return win = self.mainwindow.createWindow(self.mainwindow.editor_wintype) panel = win.getPanel('User editor') panel.openFile(url.path()) showPanel(panel) elif scheme == 'trace': TracebackDialog(self, self.outView, url.path()).show() else: self.log.warning('Strange anchor in outView: %s', url) @pyqtSlot() def on_actionPrint_triggered(self): printer = QPrinter() printdlg = QPrintDialog(printer, self) printdlg.setOption(QAbstractPrintDialog.PrintSelection) if printdlg.exec_() == QDialog.Accepted: self.outView.print_(printer) @pyqtSlot() def on_actionSave_triggered(self): fn = QFileDialog.getSaveFileName(self, 'Save', '', 'All files (*.*)')[0] if not fn: return try: fn = fn.encode(sys.getfilesystemencoding()) with open(fn, 'w') as f: f.write(self.outView.getOutputString()) except Exception as err: QMessageBox.warning(self, 'Error', 'Writing file failed: %s' % err) @pyqtSlot() def on_actionCopy_triggered(self): self.outView.copy() @pyqtSlot() def on_actionGrep_triggered(self): self.grepPanel.setVisible(True) self.grepText.setFocus() @pyqtSlot() def on_grepClose_clicked(self): self.grepPanel.setVisible(False) self.commandInput.setFocus() self.outView.scrollToBottom() def on_grepText_returnPressed(self): self.on_grepSearch_clicked() def on_grepText_escapePressed(self): self.on_grepClose_clicked() @pyqtSlot() def on_grepSearch_clicked(self): st = self.grepText.text() if not st: return found = self.outView.findNext(st, self.grepRegex.isChecked()) self.grepNoMatch.setVisible(not found) @pyqtSlot() def on_grepOccur_clicked(self): st = self.grepText.text() if not st: return self.outView.occur(st, self.grepRegex.isChecked()) @pyqtSlot(bool) def on_actionAllowLineWrap_triggered(self, checked): self.mainwindow.allowoutputlinewrap = checked if self.mainwindow.allowoutputlinewrap: self.outView.setLineWrapMode(QTextEdit.WidgetWidth) else: self.outView.setLineWrapMode(QTextEdit.NoWrap) def on_commandInput_execRequested(self, script, action): if action == 'queue': self.client.run(script) else: self.client.tell('exec', script) self.commandInput.setText('')