diff --git a/artiq/gui/entries.py b/artiq/gui/entries.py index 8b9bf4788..044092998 100644 --- a/artiq/gui/entries.py +++ b/artiq/gui/entries.py @@ -1,9 +1,10 @@ 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 +from artiq.gui.tools import LayoutWidget, disable_scroll_wheel, WheelFilter from artiq.gui.scanwidget import ScanWidget from artiq.gui.scientific_spinbox import ScientificSpinBox @@ -11,6 +12,158 @@ from artiq.gui.scientific_spinbox import ScientificSpinBox logger = logging.getLogger(__name__) +class EntryTreeWidget(QtWidgets.QTreeWidget): + 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) + 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) + self.bottom_item.setHidden(True) + + 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) + widget_item = QtWidgets.QTreeWidgetItem([key]) + if argument["tooltip"]: + widget_item.setToolTip(0, argument["tooltip"]) + widgets["entry"] = entry + widgets["widget_item"] = widget_item + + if len(self._arguments) > 1: + self.bottom_item.setHidden(False) + + 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) + self.bottom_item.setHidden(False) + 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)