import logging from collections import OrderedDict from functools import partial from PyQt5 import QtCore, QtGui, QtWidgets from artiq.gui.tools import LayoutWidget, disable_scroll_wheel, WheelFilter from artiq.gui.scanwidget import ScanWidget from artiq.gui.scientific_spinbox import ScientificSpinBox logger = logging.getLogger(__name__) class EntryTreeWidget(QtWidgets.QTreeWidget): quickStyleClicked = QtCore.pyqtSignal() def __init__(self): QtWidgets.QTreeWidget.__init__(self) self.setColumnCount(3) self.header().setStretchLastSection(False) if hasattr(self.header(), "setSectionResizeMode"): set_resize_mode = self.header().setSectionResizeMode else: set_resize_mode = self.header().setResizeMode set_resize_mode(0, QtWidgets.QHeaderView.ResizeToContents) set_resize_mode(1, QtWidgets.QHeaderView.Stretch) set_resize_mode(2, QtWidgets.QHeaderView.ResizeToContents) self.header().setVisible(False) self.setSelectionMode(self.NoSelection) self.setHorizontalScrollMode(self.ScrollPerPixel) self.setVerticalScrollMode(self.ScrollPerPixel) self.setStyleSheet("QTreeWidget {background: " + self.palette().midlight().color().name() + " ;}") self.viewport().installEventFilter(WheelFilter(self.viewport(), True)) self._groups = dict() self._arg_to_widgets = dict() self._arguments = dict() self.gradient = QtGui.QLinearGradient( 0, 0, 0, QtGui.QFontMetrics(self.font()).lineSpacing() * 2.5) self.gradient.setColorAt(0, self.palette().base().color()) self.gradient.setColorAt(1, self.palette().midlight().color()) self.bottom_item = QtWidgets.QTreeWidgetItem() self.addTopLevelItem(self.bottom_item) def set_argument(self, key, argument): self._arguments[key] = argument widgets = dict() self._arg_to_widgets[key] = widgets entry_class = procdesc_to_entry(argument["desc"]) argument["state"] = entry_class.default_state(argument["desc"]) entry = entry_class(argument) if argument["desc"].get("quickstyle"): entry.quickStyleClicked.connect(self.quickStyleClicked) widget_item = QtWidgets.QTreeWidgetItem([key]) if argument["tooltip"]: widget_item.setToolTip(0, argument["tooltip"]) widgets["entry"] = entry widgets["widget_item"] = widget_item for col in range(3): widget_item.setBackground(col, self.gradient) font = widget_item.font(0) font.setBold(True) widget_item.setFont(0, font) if argument["group"] is None: self.insertTopLevelItem(self.indexFromItem(self.bottom_item).row(), widget_item) else: self._get_group(argument["group"]).addChild(widget_item) fix_layout = LayoutWidget() widgets["fix_layout"] = fix_layout fix_layout.addWidget(entry) self.setItemWidget(widget_item, 1, fix_layout) reset_entry = QtWidgets.QToolButton() reset_entry.setToolTip("Reset to default value") reset_entry.setIcon( QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.SP_BrowserReload)) reset_entry.clicked.connect(partial(self.reset_entry, key)) disable_other_scans = QtWidgets.QToolButton() widgets["disable_other_scans"] = disable_other_scans disable_other_scans.setIcon( QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.SP_DialogResetButton)) disable_other_scans.setToolTip("Disable other scans") disable_other_scans.clicked.connect( partial(self._disable_other_scans, key)) if not isinstance(entry, ScanEntry): disable_other_scans.setVisible(False) tool_buttons = LayoutWidget() tool_buttons.layout.setRowStretch(0, 1) tool_buttons.layout.setRowStretch(3, 1) tool_buttons.addWidget(reset_entry, 1) tool_buttons.addWidget(disable_other_scans, 2) self.setItemWidget(widget_item, 2, tool_buttons) def _get_group(self, key): if key in self._groups: return self._groups[key] group = QtWidgets.QTreeWidgetItem([key]) for col in range(3): group.setBackground(col, self.palette().mid()) group.setForeground(col, self.palette().brightText()) font = group.font(col) font.setBold(True) group.setFont(col, font) self.insertTopLevelItem(self.indexFromItem(self.bottom_item).row(), group) self._groups[key] = group return group def _disable_other_scans(self, current_key): for key, widgets in self._arg_to_widgets.items(): if (key != current_key and isinstance(widgets["entry"], ScanEntry)): widgets["entry"].disable() def update_argument(self, key, argument): widgets = self._arg_to_widgets[key] # Qt needs a setItemWidget() to handle layout correctly, # simply replacing the entry inside the LayoutWidget # results in a bug. widgets["entry"].deleteLater() widgets["entry"] = procdesc_to_entry(argument["desc"])(argument) widgets["disable_other_scans"].setVisible( isinstance(widgets["entry"], ScanEntry)) widgets["fix_layout"].deleteLater() widgets["fix_layout"] = LayoutWidget() widgets["fix_layout"].addWidget(widgets["entry"]) self.setItemWidget(widgets["widget_item"], 1, widgets["fix_layout"]) self.updateGeometries() def reset_entry(self, key): procdesc = self._arguments[key]["desc"] self._arguments[key]["state"] = procdesc_to_entry(procdesc).default_state(procdesc) self.update_argument(key, self._arguments[key]) def save_state(self): expanded = [] for k, v in self._groups.items(): if v.isExpanded(): expanded.append(k) return { "expanded": expanded, "scroll": self.verticalScrollBar().value() } def restore_state(self, state): for e in state["expanded"]: try: self._groups[e].setExpanded(True) except KeyError: pass self.verticalScrollBar().setValue(state["scroll"]) class StringEntry(QtWidgets.QLineEdit): def __init__(self, argument): QtWidgets.QLineEdit.__init__(self) self.setText(argument["state"]) def update(text): argument["state"] = text self.textEdited.connect(update) @staticmethod def state_to_value(state): return state @staticmethod def default_state(procdesc): return procdesc.get("default", "") class BooleanEntry(QtWidgets.QCheckBox): def __init__(self, argument): QtWidgets.QCheckBox.__init__(self) self.setChecked(argument["state"]) def update(checked): argument["state"] = bool(checked) self.stateChanged.connect(update) @staticmethod def state_to_value(state): return state @staticmethod def default_state(procdesc): return procdesc.get("default", False) class EnumerationEntry(QtWidgets.QWidget): quickStyleClicked = QtCore.pyqtSignal() def __init__(self, argument): QtWidgets.QWidget.__init__(self) layout = QtWidgets.QHBoxLayout() self.setLayout(layout) procdesc = argument["desc"] choices = procdesc["choices"] if procdesc["quickstyle"]: self.btn_group = QtWidgets.QButtonGroup() for i, choice in enumerate(choices): button = QtWidgets.QPushButton(choice) self.btn_group.addButton(button) self.btn_group.setId(button, i) layout.addWidget(button) def submit(index): argument["state"] = choices[index] self.quickStyleClicked.emit() self.btn_group.idClicked.connect(submit) else: self.combo_box = QtWidgets.QComboBox() disable_scroll_wheel(self.combo_box) self.combo_box.addItems(choices) idx = choices.index(argument["state"]) self.combo_box.setCurrentIndex(idx) layout.addWidget(self.combo_box) def update(index): argument["state"] = choices[index] self.combo_box.currentIndexChanged.connect(update) @staticmethod def state_to_value(state): return state @staticmethod def default_state(procdesc): if "default" in procdesc: return procdesc["default"] else: return procdesc["choices"][0] class NumberEntryInt(QtWidgets.QSpinBox): def __init__(self, argument): QtWidgets.QSpinBox.__init__(self) disable_scroll_wheel(self) procdesc = argument["desc"] self.setSingleStep(procdesc["step"]) if procdesc["min"] is not None: self.setMinimum(procdesc["min"]) else: self.setMinimum(-((1 << 31) - 1)) if procdesc["max"] is not None: self.setMaximum(procdesc["max"]) else: self.setMaximum((1 << 31) - 1) if procdesc["unit"]: self.setSuffix(" " + procdesc["unit"]) self.setValue(argument["state"]) def update(value): argument["state"] = value self.valueChanged.connect(update) @staticmethod def state_to_value(state): return state @staticmethod def default_state(procdesc): if "default" in procdesc: return procdesc["default"] else: have_max = "max" in procdesc and procdesc["max"] is not None have_min = "min" in procdesc and procdesc["min"] is not None if have_max and have_min: if procdesc["min"] <= 0 < procdesc["max"]: return 0 elif have_min and not have_max: if procdesc["min"] >= 0: return procdesc["min"] elif not have_min and have_max: if procdesc["max"] < 0: return procdesc["max"] return 0 class NumberEntryFloat(ScientificSpinBox): def __init__(self, argument): ScientificSpinBox.__init__(self) disable_scroll_wheel(self) procdesc = argument["desc"] scale = procdesc["scale"] self.setDecimals(procdesc["precision"]) self.setSigFigs() self.setSingleStep(procdesc["step"]/scale) self.setRelativeStep() if procdesc["min"] is not None: self.setMinimum(procdesc["min"]/scale) else: self.setMinimum(float("-inf")) if procdesc["max"] is not None: self.setMaximum(procdesc["max"]/scale) else: self.setMaximum(float("inf")) if procdesc["unit"]: self.setSuffix(" " + procdesc["unit"]) self.setValue(argument["state"]/scale) def update(value): argument["state"] = value*scale self.valueChanged.connect(update) @staticmethod def state_to_value(state): return state @staticmethod def default_state(procdesc): if "default" in procdesc: return procdesc["default"] else: return 0.0 class _NoScan(LayoutWidget): def __init__(self, procdesc, state): LayoutWidget.__init__(self) scale = procdesc["scale"] self.value = ScientificSpinBox() disable_scroll_wheel(self.value) self.value.setDecimals(procdesc["precision"]) self.value.setSigFigs() if procdesc["global_min"] is not None: self.value.setMinimum(procdesc["global_min"]/scale) else: self.value.setMinimum(float("-inf")) if procdesc["global_max"] is not None: self.value.setMaximum(procdesc["global_max"]/scale) else: self.value.setMaximum(float("inf")) self.value.setSingleStep(procdesc["global_step"]/scale) self.value.setRelativeStep() if procdesc["unit"]: self.value.setSuffix(" " + procdesc["unit"]) self.addWidget(QtWidgets.QLabel("Value:"), 0, 0) self.addWidget(self.value, 0, 1) self.value.setValue(state["value"]/scale) def update(value): state["value"] = value*scale self.value.valueChanged.connect(update) self.repetitions = QtWidgets.QSpinBox() self.repetitions.setMinimum(1) self.repetitions.setMaximum((1 << 31) - 1) disable_scroll_wheel(self.repetitions) self.addWidget(QtWidgets.QLabel("Repetitions:"), 1, 0) self.addWidget(self.repetitions, 1, 1) self.repetitions.setValue(state["repetitions"]) def update_repetitions(value): state["repetitions"] = value self.repetitions.valueChanged.connect(update_repetitions) class _RangeScan(LayoutWidget): def __init__(self, procdesc, state): LayoutWidget.__init__(self) scale = procdesc["scale"] def apply_properties(widget): widget.setDecimals(procdesc["precision"]) if procdesc["global_min"] is not None: widget.setMinimum(procdesc["global_min"]/scale) else: widget.setMinimum(float("-inf")) if procdesc["global_max"] is not None: widget.setMaximum(procdesc["global_max"]/scale) else: widget.setMaximum(float("inf")) if procdesc["global_step"] is not None: widget.setSingleStep(procdesc["global_step"]/scale) if procdesc["unit"]: widget.setSuffix(" " + procdesc["unit"]) scanner = ScanWidget() disable_scroll_wheel(scanner) self.addWidget(scanner, 0, 0, -1, 1) start = ScientificSpinBox() start.setStyleSheet("QDoubleSpinBox {color:blue}") disable_scroll_wheel(start) self.addWidget(start, 0, 1) npoints = QtWidgets.QSpinBox() npoints.setMinimum(1) npoints.setMaximum((1 << 31) - 1) disable_scroll_wheel(npoints) self.addWidget(npoints, 1, 1) stop = ScientificSpinBox() stop.setStyleSheet("QDoubleSpinBox {color:red}") disable_scroll_wheel(stop) self.addWidget(stop, 2, 1) randomize = QtWidgets.QCheckBox("Randomize") self.addWidget(randomize, 3, 1) self.layout.setColumnStretch(0, 4) self.layout.setColumnStretch(1, 1) apply_properties(start) start.setSigFigs() start.setRelativeStep() apply_properties(stop) stop.setSigFigs() stop.setRelativeStep() apply_properties(scanner) def update_start(value): state["start"] = value*scale scanner.setStart(value) if start.value() != value: start.setValue(value) def update_stop(value): state["stop"] = value*scale scanner.setStop(value) if stop.value() != value: stop.setValue(value) def update_npoints(value): state["npoints"] = value scanner.setNum(value) if npoints.value() != value: npoints.setValue(value) def update_randomize(value): state["randomize"] = value randomize.setChecked(value) scanner.startChanged.connect(update_start) scanner.numChanged.connect(update_npoints) scanner.stopChanged.connect(update_stop) start.valueChanged.connect(update_start) npoints.valueChanged.connect(update_npoints) stop.valueChanged.connect(update_stop) randomize.stateChanged.connect(update_randomize) scanner.setStart(state["start"]/scale) scanner.setNum(state["npoints"]) scanner.setStop(state["stop"]/scale) randomize.setChecked(state["randomize"]) class _CenterScan(LayoutWidget): def __init__(self, procdesc, state): LayoutWidget.__init__(self) scale = procdesc["scale"] def apply_properties(widget): widget.setDecimals(procdesc["precision"]) if procdesc["global_min"] is not None: widget.setMinimum(procdesc["global_min"]/scale) else: widget.setMinimum(float("-inf")) if procdesc["global_max"] is not None: widget.setMaximum(procdesc["global_max"]/scale) else: widget.setMaximum(float("inf")) if procdesc["global_step"] is not None: widget.setSingleStep(procdesc["global_step"]/scale) if procdesc["unit"]: widget.setSuffix(" " + procdesc["unit"]) center = ScientificSpinBox() disable_scroll_wheel(center) apply_properties(center) center.setSigFigs() center.setRelativeStep() center.setValue(state["center"]/scale) self.addWidget(center, 0, 1) self.addWidget(QtWidgets.QLabel("Center:"), 0, 0) span = ScientificSpinBox() disable_scroll_wheel(span) apply_properties(span) span.setSigFigs() span.setRelativeStep() span.setMinimum(0) span.setValue(state["span"]/scale) self.addWidget(span, 1, 1) self.addWidget(QtWidgets.QLabel("Span:"), 1, 0) step = ScientificSpinBox() disable_scroll_wheel(step) apply_properties(step) step.setSigFigs() step.setRelativeStep() step.setMinimum(0) step.setValue(state["step"]/scale) self.addWidget(step, 2, 1) self.addWidget(QtWidgets.QLabel("Step:"), 2, 0) randomize = QtWidgets.QCheckBox("Randomize") self.addWidget(randomize, 3, 1) randomize.setChecked(state["randomize"]) def update_center(value): state["center"] = value*scale def update_span(value): state["span"] = value*scale def update_step(value): state["step"] = value*scale def update_randomize(value): state["randomize"] = value center.valueChanged.connect(update_center) span.valueChanged.connect(update_span) step.valueChanged.connect(update_step) randomize.stateChanged.connect(update_randomize) class _ExplicitScan(LayoutWidget): def __init__(self, state): LayoutWidget.__init__(self) self.value = QtWidgets.QLineEdit() self.addWidget(QtWidgets.QLabel("Sequence:"), 0, 0) self.addWidget(self.value, 0, 1) float_regexp = r"(([+-]?\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)" regexp = "(float)?( +float)* *".replace("float", float_regexp) self.value.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(regexp))) self.value.setText(" ".join([str(x) for x in state["sequence"]])) def update(text): if self.value.hasAcceptableInput(): state["sequence"] = [float(x) for x in text.split()] self.value.textEdited.connect(update) class ScanEntry(LayoutWidget): def __init__(self, argument): LayoutWidget.__init__(self) self.argument = argument self.stack = QtWidgets.QStackedWidget() self.addWidget(self.stack, 1, 0, colspan=4) procdesc = argument["desc"] state = argument["state"] self.widgets = OrderedDict() self.widgets["NoScan"] = _NoScan(procdesc, state["NoScan"]) self.widgets["RangeScan"] = _RangeScan(procdesc, state["RangeScan"]) self.widgets["CenterScan"] = _CenterScan(procdesc, state["CenterScan"]) self.widgets["ExplicitScan"] = _ExplicitScan(state["ExplicitScan"]) for widget in self.widgets.values(): self.stack.addWidget(widget) self.radiobuttons = OrderedDict() self.radiobuttons["NoScan"] = QtWidgets.QRadioButton("No scan") self.radiobuttons["RangeScan"] = QtWidgets.QRadioButton("Range") self.radiobuttons["CenterScan"] = QtWidgets.QRadioButton("Center") self.radiobuttons["ExplicitScan"] = QtWidgets.QRadioButton("Explicit") scan_type = QtWidgets.QButtonGroup() for n, b in enumerate(self.radiobuttons.values()): self.addWidget(b, 0, n) scan_type.addButton(b) b.toggled.connect(self._scan_type_toggled) selected = argument["state"]["selected"] self.radiobuttons[selected].setChecked(True) def disable(self): self.radiobuttons["NoScan"].setChecked(True) self.widgets["NoScan"].repetitions.setValue(1) @staticmethod def state_to_value(state): selected = state["selected"] r = dict(state[selected]) r["ty"] = selected return r @staticmethod def default_state(procdesc): scale = procdesc["scale"] state = { "selected": "NoScan", "NoScan": {"value": 0.0, "repetitions": 1}, "RangeScan": {"start": 0.0, "stop": 100.0*scale, "npoints": 10, "randomize": False, "seed": None}, "CenterScan": {"center": 0.*scale, "span": 100.*scale, "step": 10.*scale, "randomize": False, "seed": None}, "ExplicitScan": {"sequence": []} } if "default" in procdesc: defaults = procdesc["default"] if not isinstance(defaults, list): defaults = [defaults] state["selected"] = defaults[0]["ty"] for default in reversed(defaults): ty = default["ty"] if ty == "NoScan": state[ty]["value"] = default["value"] state[ty]["repetitions"] = default["repetitions"] elif ty == "RangeScan": state[ty]["start"] = default["start"] state[ty]["stop"] = default["stop"] state[ty]["npoints"] = default["npoints"] state[ty]["randomize"] = default["randomize"] state[ty]["seed"] = default["seed"] elif ty == "CenterScan": for key in "center span step randomize seed".split(): state[ty][key] = default[key] elif ty == "ExplicitScan": state[ty]["sequence"] = default["sequence"] else: logger.warning("unknown default type: %s", ty) return state def _scan_type_toggled(self): for ty, button in self.radiobuttons.items(): if button.isChecked(): self.stack.setCurrentWidget(self.widgets[ty]) self.argument["state"]["selected"] = ty break def procdesc_to_entry(procdesc): ty = procdesc["ty"] if ty == "NumberValue": is_int = (procdesc["precision"] == 0 and int(procdesc["step"]) == procdesc["step"] and procdesc["scale"] == 1) if is_int: return NumberEntryInt else: return NumberEntryFloat else: return { "PYONValue": StringEntry, "BooleanValue": BooleanEntry, "EnumerationValue": EnumerationEntry, "StringValue": StringEntry, "Scannable": ScanEntry }[ty]