class StatusArea(TaurusWidget): """A (scrolling) text area that displays device status, or any other string attribute.""" statusTrigger = QtCore.pyqtSignal(str) def __init__(self, parent=None, down_command=None, up_command=None, state=None): TaurusWidget.__init__(self, parent) self._setup_ui() def _setup_ui(self): hbox = QtGui.QVBoxLayout(self) self.setLayout(hbox) self.status_label = QtGui.QLabel("(No status has been read.)") self.status_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) self.status_label.setWordWrap(True) self.status_label.setAlignment(QtCore.Qt.AlignTop) status_scroll_area = QtGui.QScrollArea() status_scroll_area.setMaximumSize(QtCore.QSize(100000, 100)) status_scroll_area.setWidgetResizable(True) status_scroll_area.setWidget(self.status_label) hbox.addWidget(status_scroll_area) self.status = None self.statusTrigger.connect(self.updateStatus) def setModel(self, model): if model: split_model = model.split("/") if len(split_model) < 4: self.status = Attribute("%s/Status" % model) else: self.status = Attribute(model) self.status.addListener(self.onStatusChange) else: self.status and self.status.removeListener(self.onStatusChange) def onStatusChange(self, evt_src, evt_type, evt_value): if evt_type in [PyTango.EventType.CHANGE_EVENT, PyTango.EventType.PERIODIC_EVENT]: self.statusTrigger.emit(evt_value.value) def updateStatus(self, status): self.status_label.setText(status)
class ForcedReadTool(QtGui.QAction, BaseConfigurableClass): """ This tool provides a menu option to control the "Forced Read" period of Plot data items that implement a `setForcedReadPeriod` method (see, e.g. :meth:`TaurusTrendSet.setForcedReadPeriod`). The force-read feature consists on forcing periodic attribute reads for those attributes being plotted with a :class:`TaurusTrendSet` object. This allows to force plotting periodical updates even for attributes for which the taurus polling is not enabled. Note that this is done at the widget level and therefore does not affect the rate of arrival of events for other widgets connected to the same attributes This tool inserts an action with a spinbox and emits a `valueChanged` signal whenever the value is changed. The connection between the data items and this tool can be done manually (by connecting to the `valueChanged` signal or automatically, if :meth:`autoconnect()` is `True` (default). The autoconnection feature works by discovering the compliant data items that share associated to the plot_item. This tool is implemented as an Action, and provides a method to attach it to a :class:`pyqtgraph.PlotItem` """ valueChanged = QtCore.pyqtSignal(int) def __init__( self, parent=None, period=0, text="Change forced read period...", autoconnect=True, ): BaseConfigurableClass.__init__(self) QtGui.QAction.__init__(self, text, parent) tt = "Period between forced readings.\nSet to 0 to disable" self.setToolTip(tt) self._period = period self._autoconnect = autoconnect # register config properties self.registerConfigProperty(self.period, self.setPeriod, "period") self.registerConfigProperty( self.autoconnect, self.setAutoconnect, "autoconnect" ) # internal conections self.triggered.connect(self._onTriggered) def _onTriggered(self): period = self.period() period, ok = QtGui.QInputDialog.getInt( self.parentWidget(), "New read period", "Period (in ms) between forced readings.\nSet to 0 to disable", period, 0, 604800000, 500, ) if ok: self.setPeriod(period) def attachToPlotItem(self, plot_item): """Use this method to add this tool to a plot :param plot_item: (PlotItem) """ menu = plot_item.getViewBox().menu menu.addAction(self) self.plot_item = plot_item # force an update of period for connected trendsets self.setPeriod(self.period()) if self.autoconnect(): # enable the forced reading also for trendsets added in the future try: # requires https://github.com/pyqtgraph/pyqtgraph/pull/1388 plot_item.scene().sigItemAdded.connect(self._onAddedItem) except AttributeError: pass def _onAddedItem(self, item): if hasattr(item, "setForcedReadPeriod"): item.setForcedReadPeriod(self.period()) def autoconnect(self): """Returns autoconnect state :return: (bool) """ return self._autoconnect def setAutoconnect(self, autoconnect): """Set autoconnect state. If True, the tool will autodetect trendsets associated to the plot item and will call setForcedReadPeriod on each of them for each change. If False, it will only emit a valueChanged signal and only those connected to it will be notified of changes :param autoconnect: (bool) """ self._autoconnect = autoconnect def period(self): """Returns the current period value (in ms) :return: (int) """ return self._period def setPeriod(self, period): """Change the period value. Use 0 for disabling :param period: (int) period in ms """ self._period = period # update existing items if self.autoconnect() and self.plot_item is not None: for item in self.plot_item.listDataItems(): if hasattr(item, "setForcedReadPeriod"): item.setForcedReadPeriod(period) # emit valueChanged self.valueChanged.emit(period)
class MAXValueBar(TaurusWidget): value_trigger = QtCore.pyqtSignal(float, float) conf_trigger = QtCore.pyqtSignal() _delta = 1 def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode=designMode) self._setup_ui() self._throttle_timer = QtCore.QTimer() self._throttle_timer.setInterval(200) self._throttle_timer.setSingleShot(True) self.connect(self._throttle_timer, QtCore.SIGNAL("timeout()"), self._writeValue) self._value = None self._acc_value = 0 # accumulate fast wheel events self._last_wheel = 0 # time of last wheel event @classmethod def getQtDesignerPluginInfo(cls): ret = TaurusWidget.getQtDesignerPluginInfo() ret['module'] = 'maxvaluebar' ret['group'] = 'MAX-lab Taurus Widgets' ret['container'] = ':/designer/frame.png' ret['container'] = False return ret def _setup_ui(self): vbox = QtGui.QHBoxLayout(self) self.valuebar = ValueBarWidget() vbox.addWidget(self.valuebar) self.setLayout(vbox) self.value_trigger.connect(self.valuebar.setValue) self.conf_trigger.connect(self.updateConfig) self.setFocusPolicy(QtCore.Qt.WheelFocus) def setModel(self, model): TaurusWidget.setModel(self, model) self.updateConfig() conf = Configuration("%s?configuration" % self.model) conf.addListener(lambda *args: self.conf_trigger.emit()) def _decimalDigits(self, fmt): '''returns the number of decimal digits from a format string (or None if they are not defined)''' try: if fmt[-1].lower() in ['f', 'g'] and '.' in fmt: return int(fmt[:-1].split('.')[-1]) else: return None except: return None def updateConfig(self): conf = Configuration("%s?configuration" % self.model) # Note: could be inefficient with lots of redraws? self.valuebar.setMaximum(float_or_none(conf.max_value)) self.valuebar.setMinimum(float_or_none(conf.min_value)) self.valuebar.setWarningHigh(float_or_none(conf.max_warning)) self.valuebar.setWarningLow(float_or_none(conf.min_warning)) self.valuebar.setAlarmHigh(float_or_none(conf.max_alarm)) self.valuebar.setAlarmLow(float_or_none(conf.min_alarm)) digits = self._decimalDigits(conf.format) if digits: self._delta = pow(10, -digits) @QtCore.pyqtSlot() def _writeValue(self): if self._value: self.getModelObj().write(self._value) def throttledWrite(self, value): """Intead of writing to Tango every time the value changes, we start a timer. Writes during the timer will be accumulated and when the timer runs out, the last value is written. """ self._value = value if not self._throttle_timer.isActive(): self._throttle_timer.start() def handleEvent(self, evt_src, evt_type, evt_value): if evt_type in (PyTango.EventType.PERIODIC_EVENT, PyTango.EventType.CHANGE_EVENT): if evt_value.quality == PyTango.AttrQuality.ATTR_VALID: self.value_trigger.emit(evt_value.value, evt_value.w_value) elif evt_type in (PyTango.EventType.ATTR_CONF_EVENT, PyTango.EventType.QUALITY_EVENT): # Note: why don't I get "ATTR_CONF" events when the attribute # config changes? Seems like I get QUALITY events instead..? self.conf_trigger.emit() def wheelEvent(self, evt): model = self.getModelObj() evt.accept() numDegrees = evt.delta() / 8 numSteps = numDegrees / 15 modifiers = evt.modifiers() if modifiers & Qt.Qt.ControlModifier: numSteps *= 10 elif (modifiers & Qt.Qt.AltModifier) and model.isFloat(): numSteps *= .1 # We change the value by 1 in the least significant digit according # to the configured format. value = self.valuebar.write_value + numSteps * self._delta self.valuebar.setWriteValue(value) self.throttledWrite(value)
class DataModel(QtCore.QObject): ''' An object containing one piece of data which is intended to be shared. The data will be identified by its UID (a unique identifier known to objects that intend to access the data) In general, you are not supposed to instantiate objects of this class directly. Instead, you should interact via the :class:`SharedDataManager`, which uses :meth:`SharedDataManager.__getDataModel` to ensure that the DataModels are singletons. ''' dataChanged = QtCore.pyqtSignal(object) def __init__(self, parent, dataUID, defaultData=None): ''' creator :param parent: (QObject) the object's parent :param dataUID: (str) a unique identifier for the Data Model ''' QtCore.QObject.__init__(self, parent) self.__dataUID = dataUID self.__data = defaultData self.__isDataSet = False self.__readerSlots = [] self.__writerSignals = [] def __repr__(self): return '<DataModel object with dataUID="%s">' % self.dataUID() def dataUID(self): ''' returns the data unique identifier :return: (str) ''' return self.__dataUID def getData(self): ''' Returns the data object. :return: (object) the data object ''' return self.__data def setData(self, data): ''' sets the data object and emits a "dataChanged" signal with the data as the parameter :param data: (object) the new value for the Model's data ''' self.__data = data self.__isDataSet = True self.dataChanged.emit(self.__data) def connectReader(self, slot, readOnConnect=True): ''' Registers the given slot method to receive notifications whenever the data is changed. :param slot: (callable) a method that will be called when the data changes. This slot will be the receiver of a signal which has the data as its first argument. :param readOnConnect: (bool) if True (default) the slot will be called immediately with the current value of the data if the data has been already initialized .. seealso:: :meth:`connectWriter`, :meth:`getData` ''' self.dataChanged.connect(slot) if readOnConnect and self.__isDataSet: slot(self.__data) obj = getattr(slot, '__self__', slot) self.__readerSlots.append((weakref.ref(obj), slot.__name__)) def connectWriter(self, writer, signalname): ''' Registers the given writer object as a writer of the data. The writer is then expected to emit a `QtCore.SIGNAL(signalname)` with the new data as the first parameter. :param writer: (QObject) object that will change the data :param signalname: (str) the signal name that will notify changes of the data .. seealso:: :meth:`connectReader`, :meth:`setData` ''' try: get_signal(writer, signalname).connect(self.setData) except AttributeError: # support old-style signal self.connect(writer, QtCore.SIGNAL(signalname), self.setData) self.__writerSignals.append((weakref.ref(writer), signalname)) def disconnectWriter(self, writer, signalname): '''unregister a writer from this data model :param writer: (QObject) object to unregister :param signalname: (str) the signal that was registered .. seealso:: :meth:`SharedDataManager.disconnectWriter` ''' ok = get_signal(writer, signalname).disconnect(self.setData) self.__writerSignals.remove((weakref.ref(writer), signalname)) def disconnectReader(self, slot): ''' unregister a reader :param slot: (callable) the slot to which this was connected .. seealso:: :meth:`SharedDataManager.disconnectReader`, :meth:`getData` ''' ok = self.dataChanged.disconnect(slot) self.__readerSlots.remove((weakref.ref(slot.__self__), slot.__name__)) def isDataSet(self): '''Whether the data has been set at least once or if it is uninitialized :return: (bool) True if the data has been set. False it is uninitialized''' return self.__isDataSet def info(self): readers = ["%s::%s" % (repr(r()), s) for r, s in self.__readerSlots] writers = ["%s::%s" % (repr(r()), s) for r, s in self.__writerSignals] return "UID: %s\n\t Readers (%i):%s\n\t Writers (%i):%s\n" % (self.__dataUID, len(readers), readers, len(writers), writers) def readerCount(self): '''returns the number of currently registered readers of this model :return: (int) ''' return len(self.__readerSlots) def writerCount(self): '''returns the number of currently registered writers of this model :return: (int) ''' return len(self.__writerSignals)
class MAXLineEdit(TaurusValueLineEdit): """A TaurusValueLineEdit tweaked to fit MAXIV purposes. Changes: - The current digit (left of the cursor) can be in- or decremented by pressing the up/down arrow keys. If autoApply is activated, the value will be written on each such keypress. - The mouse wheel can be used to freely change the value. The change will occur in the least significant digit, configured by the Tango attribute format. autoApply works like above. - The widget will update the write value even if it is changed from somewhere else. The exception is if the widget is currently focused (the assumption being that the user is editing it.) """ _focus = False _wheel_delta = 1 w_value_trigger = QtCore.pyqtSignal() def __init__(self, parent=None, designMode=False): TaurusValueLineEdit.__init__(self, parent, designMode) self.setFocusPolicy(QtCore.Qt.WheelFocus) self._throttle_timer = QtCore.QTimer() self._throttle_timer.setInterval(200) self._throttle_timer.setSingleShot(True) self.connect(self._throttle_timer, QtCore.SIGNAL("timeout()"), self._writeValue) self.w_value_trigger.connect(self._updateWriteValue) def _stepBy(self, steps): """try to figure out which digit the cursor is at, and step by one in that digit.""" text = str(self.text()) cursor = len(text) - self.cursorPosition() if '.' not in self.text(): decimal = 0 else: decimal = len(text) - text.find('.') - 1 if cursor == decimal: return if cursor == len(text): return exp = cursor - decimal if cursor > decimal: exp -= 1 delta = 10**exp TaurusValueLineEdit._stepBy(self, steps * delta) self.setCursorPosition(len(self.text()) - cursor) if self._autoApply: self.writeValue() # this seems a but risky... def focusInEvent(self, event): self._focus = True TaurusValueLineEdit.focusInEvent(self, event) def focusOutEvent(self, event): self._focus = False self.resetPendingOperations() # discard unwritten changes (safer?) TaurusValueLineEdit.focusOutEvent(self, event) def _updateWriteValue(self): cursor_position = self.cursorPosition() self.setValue(self._w_value) self.setCursorPosition(cursor_position) def handleEvent(self, evt_src, evt_type, evt_value): TaurusValueLineEdit.handleEvent(self, evt_src, evt_type, evt_value) if evt_type in (PyTango.EventType.PERIODIC_EVENT, PyTango.EventType.CHANGE_EVENT): # taurus.core.taurusbasetypes.TaurusEventType.Periodic, # taurus.core.taurusbasetypes.TaurusEventType.Change): if not self._focus: self._w_value = evt_value.w_value self.w_value_trigger.emit() elif evt_type in (PyTango.EventType.ATTR_CONF_EVENT, PyTango.EventType.QUALITY_EVENT): # update the wheel delta to correspond to the LSD digits = self._decimalDigits(evt_value.format) if digits is not None: self._wheel_delta = pow(10, -digits) def _decimalDigits(self, fmt): '''returns the number of decimal digits from a format string (or None if they are not defined)''' try: if fmt[-1].lower() in ['f', 'g'] and '.' in fmt: return int(fmt[:-1].split('.')[-1]) else: return None except: return None @QtCore.pyqtSlot() def _writeValue(self): self.writeValue() def throttledWrite(self, delta): """Intead of writing to Tango every time the value changes, we start a timer. Writes during the timer will be accumulated and when the timer runs out, the last value is written. """ TaurusValueLineEdit._stepBy(self, delta) if not self._throttle_timer.isActive(): self._throttle_timer.start() def wheelEvent(self, evt): if not self.getEnableWheelEvent() or Qt.QLineEdit.isReadOnly(self): return Qt.QLineEdit.wheelEvent(self, evt) model = self.getModelObj() if model is None or not model.isNumeric(): return Qt.QLineEdit.wheelEvent(self, evt) evt.accept() numDegrees = evt.delta() / 8 numSteps = numDegrees / 15 modifiers = evt.modifiers() if modifiers & Qt.Qt.ControlModifier: numSteps *= 10 elif (modifiers & Qt.Qt.AltModifier) and model.isFloat(): numSteps *= .1 # change the value by 1 in the least significant digit according # to the configured format. self.throttledWrite(numSteps * self._wheel_delta) @classmethod def getQtDesignerPluginInfo(cls): ret = TaurusValueLineEdit.getQtDesignerPluginInfo() ret['group'] = 'MAX-lab Taurus Widgets' ret['module'] = 'maxwidgets.input' return ret def resetInitialValue(self): pass
class CyclePanel(TaurusWidget): "Panel for controlling the cycling functionality" trend_trigger = QtCore.pyqtSignal(bool) attrs = [ "CyclingTimePlateau", "CyclingIterations", "CyclingSteps", "CyclingRampTime", "NominalSetPoint" ] scale_factor = 1.1 def __init__(self, parent=None): TaurusWidget.__init__(self, parent) self._setup_ui() print('CyclePanel juste avant setmodel') # self.setModel('sys/tg_test/1') def scaleSize(self): size = self.form.scrollArea.widget().frameSize() return QtCore.QSize(size.width(), size.height() * self.scale_factor) def _setup_ui(self): vbox = QtGui.QVBoxLayout(self) self.setLayout(vbox) grid = QtGui.QGridLayout() self.form = MAXForm(withButtons=False) grid.addWidget(self.form, 0, 0, 2, 1) # rescale taurus form methode self.form.scrollArea.sizeHint = self.scaleSize self.status_label = StatusArea() grid.addWidget(self.status_label, 0, 1, 1, 1) commandbox = QtGui.QVBoxLayout(self) self.start_button = TaurusCommandButton(command="StartCycle") self.start_button.setUseParentModel(True) self.stop_button = TaurusCommandButton(command="StopCycle") self.stop_button.setUseParentModel(True) commandbox.addWidget(self.start_button) commandbox.addWidget(self.stop_button) grid.addLayout(commandbox, 1, 1, 1, 1) vbox.addLayout(grid) self.trend = TaurusTrend() vbox.addWidget(self.trend, stretch=1) self.trend_trigger.connect(self.set_trend_paused) self.cyclingState = None def setModel(self, device): print self.__class__.__name__, "setModel", device TaurusWidget.setModel(self, device) # self.state_button.setModel(device) if device: self.form.setModel( ["%s/%s" % (device, attribute) for attribute in self.attrs]) self.status_label.setModel("%s/cyclingStatus" % device) ps = str(PyTango.Database().get_device_property( device, "PowerSupplyProxy")["PowerSupplyProxy"][0]) self.trend.setPaused() self.trend.setModel(["%s/Current" % ps]) self.trend.setForcedReadingPeriod(0.2) self.trend.showLegend(True) # let's pause the trend when not cycling self.cyclingState = self.getModelObj().getAttribute("cyclingState") self.cyclingState.addListener(self.handle_cycling_state) else: if self.cyclingState: self.cyclingState.removeListener(self.handle_cycling_state) self.trend.setModel(None) self.status_label.setModel(None) def handle_cycling_state(self, evt_src, evt_type, evt_value): if evt_type in [ PyTango.EventType.CHANGE_EVENT, PyTango.EventType.PERIODIC_EVENT ]: self.trend_trigger.emit(evt_value.value) def set_trend_paused(self, value): self.trend.setForcedReadingPeriod(0.2 if value else -1) self.trend.setPaused(not value)
class TaurusDevCombo(TaurusWidget): try: modelChosen = QtCore.pyqtSignal() except: modelChosen = MyQtSignal('modelChosen') def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode=designMode) self.loadUi() self._selectedDevice = "" if hasattr(self.modelChosen, '_parent'): self.modelChosen._parent = self try: self._ui.selectorCombo.currentIndexChanged.connect(self.selection) except Exception as e: self.warning("Using deprecated connect signal") Qt.QObject.connect(self._ui.selectorCombo, Qt.SIGNAL('currentIndexChanged(int)'), self.selection) @classmethod def getQtDesignerPluginInfo(cls): ret = TaurusWidget.getQtDesignerPluginInfo() ret['module'] = 'widgets.TaurusDevCombo' ret['group'] = 'Taurus Views' ret['container'] = ':/designer/frame.png' ret['container'] = False return ret def setModel(self, model): self.getDeviceListByDeviceServerName(model) devNames = list(self._deviceNames.keys()) devNames.sort() self._ui.selectorCombo.addItems(devNames) self.selection(devNames[0]) def getDeviceListByDeviceServerName(self, deviceServerName): authority = Authority() foundInstances = authority.getServerNameInstances(deviceServerName) self.info("by %s found %d instances: %s." % (deviceServerName, len(foundInstances), ','.join("%s" % instance.name() for instance in foundInstances))) self._deviceNames = {} for instance in foundInstances: for i, devName in enumerate(instance.getDeviceNames()): if not devName.startswith('dserver'): self._deviceNames[devName] = instance.getClassNames()[i] return list(self._deviceNames.keys()) def selection(self, devName): if isinstance(devName, int): devName = self._ui.selectorCombo.currentText() if devName not in self._deviceNames.keys(): self.warning("Selected device is not in the list " "of devices found!") self.info("selected %s" % (devName)) self._selectedDevice = devName self.modelChosen.emit() def getSelectedDeviceName(self): # self.debug("Requested which device was selected") return self._selectedDevice def getSelectedDeviceClass(self): try: return self._deviceNames[self._selectedDevice] except: self.error("Uou! As the selected device is not in the device " "instances found, its class is unknown") return "unknown"
class ForcedReadTool(QtGui.QWidgetAction, BaseConfigurableClass): """ This tool provides a menu option to control the "Forced Read" period of Plot data items that implement a `setForcedReadPeriod` method (see, e.g. :meth:`TaurusTrendSet.setForcedReadPeriod`). The force-read feature consists on forcing periodic attribute reads for those attributes being plotted with a :class:`TaurusTrendSet` object. This allows to force plotting periodical updates even for attributes for which the taurus polling is not enabled. Note that this is done at the widget level and therefore does not affect the rate of arrival of events for other widgets connected to the same attributes This tool inserts an action with a spinbox and emits a `valueChanged` signal whenever the value is changed. The connection between the data items and this tool can be done manually (by connecting to the `valueChanged` signal or automatically, if :meth:`autoconnect()` is `True` (default). The autoconnection feature works by discovering the compliant data items that share associated to the plot_item. This tool is implemented as an Action, and provides a method to attach it to a :class:`pyqtgraph.PlotItem` """ valueChanged = QtCore.pyqtSignal(int) def __init__(self, parent=None, period=0, text="Forced read", autoconnect=True): BaseConfigurableClass.__init__(self) QtGui.QWidgetAction.__init__(self, parent) # defining the widget self._w = QtGui.QWidget() self._w.setLayout(QtGui.QHBoxLayout()) tt = "Period between forced readings.\nSet to 0 to disable" self._w.setToolTip(tt) self._label = QtGui.QLabel(text) self._w.layout().addWidget(self._label) self._sb = QtGui.QSpinBox() self._w.layout().addWidget(self._sb) self._sb.setRange(0, 604800000) self._sb.setValue(period) self._sb.setSingleStep(500) self._sb.setSuffix(" ms") self._sb.setSpecialValueText("disabled") self._autoconnect = autoconnect self.setDefaultWidget(self._w) # register config properties self.registerConfigProperty(self.period, self.setPeriod, "period") self.registerConfigProperty(self.autoconnect, self.setAutoconnect, "autoconnect") # internal conections self._sb.valueChanged[int].connect(self._onValueChanged) def _onValueChanged(self, period): """emit valueChanged and update all associated trendsets (if self.autoconnect=True """ self.valueChanged.emit(period) if self.autoconnect() and self.plot_item is not None: for item in self.plot_item.listDataItems(): if hasattr(item, "setForcedReadPeriod"): item.setForcedReadPeriod(period) def attachToPlotItem(self, plot_item): """Use this method to add this tool to a plot :param plot_item: (PlotItem) """ menu = plot_item.getViewBox().menu menu.addAction(self) self.plot_item = plot_item # force an update of period for connected trendsets self._onValueChanged(self.period()) def autoconnect(self): """Returns autoconnect state :return: (bool) """ return self._autoconnect def setAutoconnect(self, autoconnect): """Set autoconnect state. If True, the tool will autodetect trendsets associated to the plot item and will call setForcedReadPeriod on each of them for each change. If False, it will only emit a valueChanged signal and only those connected to it will be notified of changes :param autoconnect: (bool) """ self._autoconnect = autoconnect def period(self): """Returns the current period value (in ms) :return: (int) """ return self._sb.value() def setPeriod(self, value): """Change the period value. Use 0 for disabling :param period: (int) period in ms """ self._sb.setValue(value)
class AttributeColumnsTable(TaurusWidget): """Display several 1D spectrum attributes belonging to the same device as columns in a table.""" trigger = QtCore.pyqtSignal((int, int)) def __init__(self, parent=None): TaurusWidget.__init__(self, parent) self._setup_ui() def _setup_ui(self): hbox = QtGui.QHBoxLayout(self) self.setLayout(hbox) self.table = QtGui.QTableWidget() self.table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) hbox.addWidget(self.table) self.trigger.connect(self.updateColumn) self.attributes = [] self._columns = [] self._values = {} self._config = {} def setModel(self, attrs): if not attrs: for att, col in zip(self.attributes, self._columns): att and att.removeListener(col.event_received) else: try: TaurusWidget.setModel(self, attrs[0].rsplit("/", 1)[0]) self.attributes = [Attribute(a) for a in attrs] self.table.setColumnCount(len(attrs)) fmt = "%s [%s]" labels = [] for a in self.attributes: config = a.getConfig() label = fmt % (config.getLabel(), config.getUnit()) labels.append(label) self.table.setHorizontalHeaderLabels(labels) header = self.table.horizontalHeader() header.setResizeMode(QtGui.QHeaderView.Stretch) # Check if there are any columns at all row_lengths = [len(a.read().value) for a in self.attributes if a.read().quality == PyTango.AttrQuality.ATTR_VALID] if not any(row_lengths): return None self.table.setRowCount(max(row_lengths)) self._values = {} self._config = {} self._columns = [] for i, att in enumerate(self.attributes): # JFF: this is a workaround for a behavior in Taurus. Just # adding a new listener to each attribute does not work, the # previous ones get removed for some reason. col = AttributeColumn(self, i) self._columns.append(col) # keep reference to prevent GC att.addListener(col.event_received) except PyTango.DevFailed: pass def keyPressEvent(self, e): "Copy selected cells to the clipboard on Ctrl+C" if (e.modifiers() & QtCore.Qt.ControlModifier): selected = self.table.selectedRanges() if e.key() == QtCore.Qt.Key_C: s = "" for r in xrange(selected[0].topRow(), selected[0].bottomRow() + 1): for c in xrange(selected[0].leftColumn(), selected[0].rightColumn() + 1): try: s += str(self.table.item(r, c).text()) + "\t" except AttributeError: s += "\t" s = s[:-1] + "\n" # eliminate last '\t' clipboard = QtGui.QApplication.clipboard() clipboard.setText(s) def onEvent(self, column, evt_src, evt_type, evt_value): if evt_type in [PyTango.EventType.CHANGE_EVENT, PyTango.EventType.PERIODIC_EVENT]: self._values[column] = evt_value self.trigger.emit(column) if isinstance(evt_value, PyTango.DeviceAttributeConfig): self._config[column] = evt_value def updateColumn(self, column): if not self._values: return # when does this happen? data = self._values[column] for row, value in enumerate(data.value): if not isnan(value): cfg = self._config.get(column) if cfg and cfg.format != "Not specified": item = QtGui.QTableWidgetItem(cfg.format % value) else: item = QtGui.QTableWidgetItem(str(value)) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) else: item = QtGui.QTableWidgetItem("NaN") item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) item.setBackgroundColor(QtGui.QColor(220, 220, 220)) item.setTextAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.table.setItem(row, column, item)
class ToggleButton(TaurusWidget): """A button that has two states, pressed and unpressed. When pressing it, the 'down' command is run, and when unpressing it the 'up' command is run. The 'pressedness' of the button is connected to a given Tango state, e.g. ON.""" state_trigger = QtCore.pyqtSignal(bool) def __init__(self, parent=None, down_command=None, up_command=None, state=None): TaurusWidget.__init__(self, parent) self._down_command = down_command self._up_command = up_command self._state = state self._setup_ui() def _setup_ui(self): hbox = QtGui.QHBoxLayout(self) self.setLayout(hbox) self.button = QtGui.QPushButton() #self.button.setText() self.button.setCheckable(True) hbox.addWidget(self.button) self.button.clicked.connect(self.onClick) self.state_trigger.connect(self.change_state) def setModel(self, model): TaurusWidget.setModel(self, model) if model: m = self.getModelObj() self.down_command = getattr(m, self._down_command) self.up_command = getattr(m, self._up_command) self.state = m.getAttribute("State") self.state.addListener(self.handle_state_event) else: if self.state: self.state.removeListener(self.handle_state_event) def onClick(self): print "state is", self.state.read() pressed = self.state.read().value == self._state print "pressed", pressed if pressed: print "running up_commnad", self._up_command self.up_command() else: print "running down_command", self._down_command self.down_command() def change_state(self, new_state): print "change_state", new_state self.button.setChecked(new_state) self.button.setText((self._down_command, self._up_command)[new_state]) def handle_state_event(self, evt_src, evt_type, evt_value): if evt_type in [PyTango.EventType.CHANGE_EVENT, PyTango.EventType.PERIODIC_EVENT]: print "state", self._state self.state_trigger.emit(evt_value.value == self._state)
class DeviceRowsTable(TaurusWidget): """A widget that displays a table where each row displays a device, and the values of selected attributes.""" STATE_COLORS = { PyTango.DevState.ON: (0, 255, 0), PyTango.DevState.OFF: (255, 255, 255), PyTango.DevState.CLOSE: (255, 255, 255), PyTango.DevState.OPEN: (0, 255, 0), PyTango.DevState.INSERT: (255, 255, 255), PyTango.DevState.EXTRACT: (0, 255, 0), PyTango.DevState.MOVING: (128, 160, 255), PyTango.DevState.STANDBY: (255, 255, 0), PyTango.DevState.FAULT: (255, 0, 0), PyTango.DevState.INIT: (204, 204, 122), PyTango.DevState.RUNNING: (128, 160, 255), PyTango.DevState.ALARM: (255, 140, 0), PyTango.DevState.DISABLE: (255, 0, 255), PyTango.DevState.UNKNOWN: (128, 128, 128), None: (128, 128, 128) } trigger = QtCore.pyqtSignal(int, int) def __init__(self, parent=None): TaurusWidget.__init__(self, parent) self._setup_ui() def _setup_ui(self): hbox = QtGui.QHBoxLayout(self) self.setLayout(hbox) self.table = QtGui.QTableWidget(parent=self) hbox.addWidget(self.table) self.trigger.connect(self.update_item) self.table.itemClicked.connect(self.on_item_clicked) self._items = {} self.attributes = {} def keyPressEvent(self, e): "Copy selected cells to the clipboard on Ctrl+C" if (e.modifiers() & QtCore.Qt.ControlModifier): selected = self.table.selectedRanges() if e.key() == QtCore.Qt.Key_C: s = "" for r in xrange(selected[0].topRow(), selected[0].bottomRow() + 1): for c in xrange(selected[0].leftColumn(), selected[0].rightColumn() + 1): try: s += str(self.table.item(r, c).text()) + "\t" except AttributeError: s += "\t" s = s[:-1] + "\n" # eliminate last '\t' clipboard = QtGui.QApplication.clipboard() clipboard.setText(s) def setModel(self, devices, attributes=[]): if not devices: for dev, attrs in self.attributes.items(): for att in attrs: att and att.removeListener( self._items[dev][att.name].event_received) else: try: #TaurusWidget.setModel(self, attrs[0].rsplit("/", 1)[0]) attrnames = [a[0] if isinstance(a, tuple) else a for a in attributes] self.attributes = dict((dev, [Attribute("%s/%s" % (dev, a)) for a in attrnames]) for dev in devices) self.table.setColumnCount(len(attributes) + 1) colnames = [a[1] if isinstance(a, tuple) else a for a in attributes] labels = ["Device"] + colnames self.table.setHorizontalHeaderLabels(labels) header = self.table.horizontalHeader() header.setResizeMode(QtGui.QHeaderView.Stretch) # Check if there are any columns at all self.table.setRowCount(len(devices)) for r, (dev, attrs) in enumerate(self.attributes.items()): item = QtGui.QTableWidgetItem(dev) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.table.setItem(r, 0, item) for c, att in enumerate(attrs, 1): # JFF: this is a workaround for a behavior in Taurus. Just # adding a new listener to each attribute does not work, the # previous ones get removed for some reason. item = TableItem(self.trigger) self.table.setItem(r, c, item) att.addListener(item.event_received) except PyTango.DevFailed: pass def update_item(self, row, column): item = self.table.item(row, column) value, config = item.value, item.config # Set text if config and config.format != "Not specified": item.setText(config.format % value.value) else: item.setText(str(value.value)) # Set flags and state if item.writable_boolean: item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) state = QtCore.Qt.Checked if value.w_value else QtCore.Qt.Unchecked item.setCheckState(state) else: item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) # Set background color if value.type == PyTango.CmdArgType.DevState: if value.value in self.STATE_COLORS: item.setBackgroundColor(QtGui.QColor(*self.STATE_COLORS[value.value])) def on_item_clicked(self, item): # Not a writable item if not isinstance(item, TableItem) or not item.writable_boolean: return button_state = (item.checkState() == QtCore.Qt.Checked) value_state = item.value.w_value if button_state != value_state: item.source.write(button_state)
class SwitchBoardPanel(TaurusWidget): modeChanged = QtCore.pyqtSignal() typeChanged = QtCore.pyqtSignal() def __init__(self, parent=None): TaurusWidget.__init__(self, parent) self.loadUi() self.statusLabel.setUseParentModel(True) self.statusLabel.setModel('/Status') self.modeCB.setUseParentModel(True) self.modeCB.setModel('/Mode') self.type_attribute = None self.mode_attribute = None self.type_value = None self.mode_value = None self.modelChanged.connect(self.onModelChanged) self.modeChanged.connect(self.onModeChanged) self.typeChanged.connect(self.onTypeChanged) def eventReceived(self, evt_src, evt_type, evt_value): if not hasattr(evt_value, 'value'): return # We can't interact with QWidgets from here # (calling thread is not a QThread). Emit signals instead. if evt_src is self.type_attribute and \ evt_value.value != self.type_value: self.type_value = evt_value.value self.typeChanged.emit() if evt_src is self.mode_attribute and \ evt_value.value != self.mode_value: self.mode_value = evt_value.value self.modeChanged.emit() def onModelChanged(self, model): if self.mode_attribute: self.mode_attribute.removeListener(self) self.mode_attribute = None if self.type_attribute: self.type_attribute.removeListener(self) self.type_attribute = None device = self.getModelObj() if device: self.mode_attribute = device.getAttribute('Mode') self.mode_attribute.addListener(self) self.type_attribute = device.getAttribute('Type') self.type_attribute.addListener(self) def onTypeChanged(self): # fetch list of valid mode names device = self.getModelObj() if device: modes = device.command_inout('getAttrStringValueList', 'Mode') else: modes = [] # update combo box choices = [(m, m) for m in modes] self.modeCB.setValueNames(choices) # update image self.onModeChanged() def onModeChanged(self): # update image image_map = BOARD_MODES.get(self.type_value) if image_map: svg = image_map[self.mode_value] else: svg = ':/unknown.svg' self.svgWidget.load(svg)
class BufferSizeTool(QtGui.QAction, BaseConfigurableClass): """ This tool provides a menu option to control the "Maximum buffer" of Plot data items that implement a `setBufferSize` method (see, e.g. :meth:`TaurusTrendSet.setBufferSize`). This tool inserts an action with a spinbox and emits a `valueChanged` signal whenever the value is changed. The connection between the data items and this tool can be done manually (by connecting to the `valueChanged` signal or automatically, if :meth:`autoconnect()` is `True` (default). The autoconnection feature works by discovering the compliant data items that are associated to the plot_item. This tool is implemented as an Action, and provides a method to attach it to a :class:`pyqtgraph.PlotItem` """ valueChanged = QtCore.pyqtSignal(int) def __init__( self, parent=None, buffer_size=65536, text="Change buffers size...", autoconnect=True, ): BaseConfigurableClass.__init__(self) QtGui.QAction.__init__(self, text, parent) self.setToolTip("Maximum number of points for each trend") self._maxSize = buffer_size self._autoconnect = autoconnect # register config properties self.registerConfigProperty(self.bufferSize, self.setBufferSize, "buffer_size") self.registerConfigProperty(self.autoconnect, self.setAutoconnect, "autoconnect") # internal conections self.triggered.connect(self._onTriggered) def _onEdittingFinished(self): """ emit valueChanged and update all associated trendsets (if self.autoconnect=True """ buffer_size = self.bufferSize() self.valueChanged.emit(buffer_size) if self.autoconnect() and self.plot_item is not None: for item in self.plot_item.listDataItems(): if hasattr(item, "setBufferSize"): item.setBufferSize(buffer_size) def _onTriggered(self): maxSize = self.bufferSize() maxSize, ok = QtGui.QInputDialog.getInt( self.parentWidget(), "New buffer data size", "Enter the number of points to be kept in memory for each curve", maxSize, 2, 10000000, 1000, ) if ok: self.setBufferSize(maxSize) def attachToPlotItem(self, plot_item): """Use this method to add this tool to a plot :param plot_item: (PlotItem) """ menu = plot_item.getViewBox().menu menu.addAction(self) self.plot_item = plot_item # ensure that current items are set to max size self.setBufferSize(self.bufferSize()) if self.autoconnect(): # enable the buffer limit also for trendsets added in the future try: # requires https://github.com/pyqtgraph/pyqtgraph/pull/1388 plot_item.scene().sigItemAdded.connect(self._onAddedItem) except AttributeError: pass def _onAddedItem(self, item): if hasattr(item, "setBufferSize"): item.setBufferSize(self.bufferSize()) def autoconnect(self): """Returns autoconnect state :return: (bool) """ return self._autoconnect def setAutoconnect(self, autoconnect): """Set autoconnect state. If True, the tool will autodetect trendsets associated to the plot item and will call setBufferSize on each of them for each change. If False, it will only emit a valueChanged signal and only those connected to it will be notified of changes :param autoconnect: (bool) """ self._autoconnect = autoconnect def bufferSize(self): """Returns the current buffer_size value :return: (int) """ return self._maxSize def setBufferSize(self, buffer_size): """Change the buffer_size value. :param buffer_size: (int) buffer_size """ self._maxSize = buffer_size # update existing items if self.autoconnect() and self.plot_item is not None: for item in self.plot_item.listDataItems(): if hasattr(item, "setBufferSize"): item.setBufferSize(buffer_size) # emit a valueChanged signal self.valueChanged.emit(buffer_size)