class JSInterface(QtCore.QObject): """ Interface between python and a webview's javascript. All methods decorated with "pyqtSlot" on this class can be called from the JS side. """ registered = QtCore.pyqtSignal(str, str) visibility = QtCore.pyqtSignal(str, bool) rightclicked = QtCore.pyqtSignal(str, str) leftclicked = QtCore.pyqtSignal(str, str) evaljs = QtCore.pyqtSignal(str) lock = Lock() def __init__(self, frame, parent=None): self.frame = frame super(JSInterface, self).__init__(parent) self.evaljs.connect(self.evaluate_js) # thread safety def evaluate_js(self, js): #print "JS", js with self.lock: self.frame.evaluateJavaScript(js) @QtCore.pyqtSlot(str, str) def left_click(self, kind, name): self.leftclicked.emit(kind, name) @QtCore.pyqtSlot(str, str) def right_click(self, kind, name): self.rightclicked.emit(kind, name) @QtCore.pyqtSlot(str, str) def register(self, kind, name): "inform the widget about an item" self.registered.emit(kind, name) def select(self, kind, names): "set an item as selected" self.evaljs.emit("Synoptic.unselectAll()") for name in names: self.evaljs.emit("Synoptic.select(%r, %r)" % (str(kind), str(name))) @QtCore.pyqtSlot(str, bool) def visible(self, name, value=True): "Update the visibility of something" self.visibility.emit(name, value) def load(self, svg, section=None): "Load an SVG file" if section: self.evaljs.emit("Synoptic.load(%r, %r)" % (svg, section)) else: self.evaljs.emit("Synoptic.load(%r)" % svg)
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
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)
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)
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_command", 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_spec != "Not specified") and \ (config.format_spec != "!s"): data_format = "%" + config.format_spec item.setText(data_format % value.rvalue) else: item.setText(str(value.rvalue)) # 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.wvalue else QtCore.Qt.Unchecked item.setCheckState(state) else: item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) # Set background color if config.type == 5: # DevState if value.rvalue in self.STATE_COLORS: item.setBackgroundColor(QtGui.QColor(*self.STATE_COLORS[value.rvalue])) 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)
def scaleSize(self): size = self.form.scrollArea.widget().frameSize() return QtCore.QSize(size.width(), size.height() * self.scale_factor)
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() 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 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)