def __init__(self, manager): super().__init__(manager.main_window) self.setModal(True) self.manager = manager self.setWindowTitle("Quick open...") layout = QtWidgets.QGridLayout(self) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) # Find matching experiment names. Open experiments are preferred to # matches from the repository to ease quick window switching. open_exps = list(self.manager.open_experiments.keys()) repo_exps = set("repo:" + k for k in self.manager.explist.keys()) - set(open_exps) choices = [(o, 100) for o in open_exps] + [(r, 0) for r in repo_exps] self.select_widget = FuzzySelectWidget(choices) layout.addWidget(self.select_widget) self.select_widget.aborted.connect(self.close) self.select_widget.finished.connect(self._open_experiment) font_metrics = QtGui.QFontMetrics(self.select_widget.line_edit.font()) self.select_widget.setMinimumWidth(font_metrics.averageCharWidth() * 70)
def _make_add_override_prompt_item(self): self._override_prompt_item = QtWidgets.QTreeWidgetItem() self.addTopLevelItem(self._override_prompt_item) # Layout to display button/prompt label, depending on which one is active. left = LayoutWidget() self._add_override_button = QtWidgets.QToolButton() self._add_override_button.setIcon(self._add_override_icon) self._add_override_button.clicked.connect(self._set_override_line_active) self._add_override_button.setShortcut(QtCore.Qt.CTRL + QtCore.Qt.Key_T) left.addWidget(self._add_override_button, 0, 0) self._add_override_prompt_label = QtWidgets.QLabel("Add parameter:") left.addWidget(self._add_override_prompt_label, 0, 0) left.layout.setColumnStretch(0, 0) left.layout.setColumnStretch(1, 1) self.setItemWidget(self._override_prompt_item, 0, left) prompt = LayoutWidget() self._add_override_prompt_box = FuzzySelectWidget([]) self._add_override_prompt_box.finished.connect( lambda a: self._make_override_item(*self._param_choice_map[a])) self._add_override_prompt_box.aborted.connect(self._set_override_line_idle) prompt.addWidget(self._add_override_prompt_box) self.setItemWidget(self._override_prompt_item, 1, prompt)
class _QuickOpenDialog(QtWidgets.QDialog): """Modal dialog for opening/submitting experiments from a FuzzySelectWidget.""" closed = QtCore.pyqtSignal() def __init__(self, manager): super().__init__(manager.main_window) self.setModal(True) self.manager = manager self.setWindowTitle("Quick open...") layout = QtWidgets.QGridLayout(self) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) # Find matching experiment names. Open experiments are preferred to # matches from the repository to ease quick window switching. open_exps = list(self.manager.open_experiments.keys()) repo_exps = set("repo:" + k for k in self.manager.explist.keys()) - set(open_exps) choices = [(o, 100) for o in open_exps] + [(r, 0) for r in repo_exps] self.select_widget = FuzzySelectWidget(choices) layout.addWidget(self.select_widget) self.select_widget.aborted.connect(self.close) self.select_widget.finished.connect(self._open_experiment) font_metrics = QtGui.QFontMetrics(self.select_widget.line_edit.font()) self.select_widget.setMinimumWidth(font_metrics.averageCharWidth() * 70) def done(self, r): if self.select_widget: self.select_widget.abort() self.closed.emit() QtWidgets.QDialog.done(self, r) def _open_experiment(self, exp_name, modifiers): if modifiers & QtCore.Qt.ControlModifier: try: self.manager.submit(exp_name) except: # Not all open_experiments necessarily still exist in the explist # (e.g. if the repository has been re-scanned since). logger.warning("failed to submit experiment '%s'", exp_name, exc_info=True) else: self.manager.open_experiment(exp_name) self.close()
class ArgumentEditor(QtWidgets.QTreeWidget): def __init__(self, manager, dock, expurl): super().__init__() self.manager = manager self.expurl = expurl 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())) self._bg_gradient = QtGui.QLinearGradient( 0, 0, 0, QtGui.QFontMetrics(self.font()).lineSpacing()) self._bg_gradient.setColorAt(0, self.palette().base().color()) self._bg_gradient.setColorAt(1, self.palette().midlight().color()) self._save_timer = QtCore.QTimer(self) self._save_timer.timeout.connect(self._save_to_argument) self._param_entries = OrderedDict() self._groups = dict() self._arg_to_widgets = dict() self._override_items = dict() def icon_path(name): return os.path.join(os.path.dirname(os.path.abspath(__file__)), "icons", name) self._add_override_icon = QtGui.QIcon(icon_path("list-add-32.png")) self._remove_override_icon = QtGui.QIcon(icon_path("list-remove-32.png")) self._randomise_scan_icon = QtGui.QIcon( icon_path("media-playlist-shuffle-32.svg")) self._default_value_icon = self.style().standardIcon( QtWidgets.QStyle.SP_BrowserReload) self._disable_scans_icon = self.style().standardIcon( QtWidgets.QStyle.SP_DialogResetButton) self._arguments = self.manager.get_submission_arguments(self.expurl) ndscan_params, vanilla_args = _try_extract_ndscan_params(self._arguments) if not ndscan_params: self.addTopLevelItem( QtWidgets.QTreeWidgetItem(["Error: Parameter metadata not found."])) else: self._ndscan_params = ndscan_params self.override_separator = None self._build_shortened_fqns() self.scan_options = None if "scan" in ndscan_params: self.scan_options = ScanOptions(ndscan_params["scan"]) for fqn, path in ndscan_params["always_shown"]: self._make_param_items(fqn, path, True) for name, argument in vanilla_args.items(): self._make_vanilla_argument_item(name, argument) self.override_separator = self._make_line_separator() self._make_add_override_prompt_item() self._set_override_line_idle() for ax in ndscan_params.get("scan", {}).get("axes", []): self._make_override_item(ax["fqn"], ax["path"]) for fqn, overrides in ndscan_params["overrides"].items(): for o in overrides: self._make_override_item(fqn, o["path"]) self._make_line_separator() if self.scan_options: scan_options_group = self._make_group_header_item("Scan options") self.addTopLevelItem(scan_options_group) for widget in self.scan_options.get_widgets(): twi = QtWidgets.QTreeWidgetItem() scan_options_group.addChild(twi) self.setItemWidget(twi, 1, widget) buttons_item = QtWidgets.QTreeWidgetItem() self.addTopLevelItem(buttons_item) buttons_item.setFirstColumnSpanned(True) recompute_arguments = QtWidgets.QPushButton("Recompute all arguments") recompute_arguments.setIcon(self._default_value_icon) recompute_arguments.clicked.connect(dock._recompute_arguments_clicked) load_hdf5 = QtWidgets.QPushButton("Load HDF5") load_hdf5.setIcon(QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.SP_DialogOpenButton)) load_hdf5.clicked.connect(dock._load_hdf5_clicked) disable_scans = QtWidgets.QPushButton("Disable all scans") disable_scans.setIcon(self._disable_scans_icon) disable_scans.clicked.connect(self.disable_all_scans) disable_scans.setShortcut(QtCore.Qt.CTRL + QtCore.Qt.Key_R) buttons = LayoutWidget() buttons.addWidget(recompute_arguments, col=1) buttons.addWidget(load_hdf5, col=2) buttons.addWidget(disable_scans, col=3) buttons.layout.setColumnStretch(0, 1) buttons.layout.setColumnStretch(1, 0) buttons.layout.setColumnStretch(2, 0) buttons.layout.setColumnStretch(3, 0) buttons.layout.setColumnStretch(4, 1) self.setItemWidget(buttons_item, 0, buttons) 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"]) def about_to_submit(self): self._save_to_argument() def about_to_close(self): self._save_to_argument() def disable_all_scans(self): for entry in self._param_entries.values(): entry.disable_scan() def _make_param_items(self, fqn, path, show_always, insert_at_idx=-1): if (fqn, path) in self._param_entries: return schema = self._schema_for_fqn(fqn) added_item_count = 0 def add_item(widget_item): nonlocal added_item_count group = schema.get("group", None) if not group: if insert_at_idx == -1: self.addTopLevelItem(widget_item) else: self.insertTopLevelItem(insert_at_idx + added_item_count, widget_item) added_item_count += 1 else: self._ensure_group_widget(group).addChild(widget_item) id_string = self._param_display_name(fqn, path) id_item = QtWidgets.QTreeWidgetItem([id_string]) add_item(id_item) for col in range(3): id_item.setBackground(col, self._bg_gradient) id_item.setFirstColumnSpanned(True) id_item.setForeground(0, self.palette().mid()) main_item = QtWidgets.QTreeWidgetItem([schema["description"]]) add_item(main_item) # Render description in bold. font = main_item.font(0) font.setBold(True) main_item.setFont(0, font) entry = self._make_override_entry(fqn, path) entry.read_from_params(self._ndscan_params, self.manager.datasets) entry.value_changed.connect(self._set_save_timer) self._param_entries[(fqn, path)] = entry self.setItemWidget(main_item, 1, entry) buttons = LayoutWidget() reset_default = QtWidgets.QToolButton() reset_default.setToolTip("Reset parameter to default value") reset_default.setIcon(QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.SP_BrowserReload)) reset_default.clicked.connect(partial(self._reset_entry_to_default, fqn, path)) buttons.addWidget(reset_default, col=0) remove_override = QtWidgets.QToolButton() remove_override.setIcon(self._remove_override_icon) remove_override.setToolTip("Remove this parameter override") remove_override.clicked.connect(partial(self._remove_override, fqn, path)) buttons.addWidget(remove_override, col=1) self.setItemWidget(main_item, 2, buttons) if show_always: sp = remove_override.sizePolicy() sp.setRetainSizeWhenHidden(True) remove_override.setSizePolicy(sp) remove_override.setVisible(False) return id_item, main_item def _make_vanilla_argument_item(self, name, argument): if name in self._arg_to_widgets: logger.warning("Argument with name '%s' already exists, skipping.", name) return widgets = dict() self._arg_to_widgets[name] = widgets entry = procdesc_to_entry(argument["desc"])(argument) widget_item = QtWidgets.QTreeWidgetItem([name]) if argument["tooltip"]: widget_item.setToolTip(1, argument["tooltip"]) widgets["entry"] = entry widgets["widget_item"] = widget_item for col in range(3): widget_item.setBackground(col, self._bg_gradient) font = widget_item.font(0) font.setBold(True) widget_item.setFont(0, font) if argument["group"] is None: self.addTopLevelItem(widget_item) else: self._ensure_group_widget(argument["group"]).addChild(widget_item) fix_layout = LayoutWidget() widgets["fix_layout"] = fix_layout fix_layout.addWidget(entry) self.setItemWidget(widget_item, 1, fix_layout) buttons = LayoutWidget() recompute_argument = QtWidgets.QToolButton() recompute_argument.setToolTip("Re-run the experiment's build " "method and take the default value") recompute_argument.setIcon(self._default_value_icon) recompute_argument.clicked.connect( partial(self._recompute_vanilla_argument_clicked, name)) buttons.addWidget(recompute_argument) buttons.layout.setColumnStretch(0, 0) buttons.layout.setColumnStretch(1, 1) self.setItemWidget(widget_item, 2, buttons) def _make_line_separator(self): f = QtWidgets.QFrame(self) f.setMinimumHeight(15) f.setFrameShape(QtWidgets.QFrame.HLine) f.setFrameShadow(QtWidgets.QFrame.Sunken) f.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) wi = QtWidgets.QTreeWidgetItem() self.addTopLevelItem(wi) wi.setFirstColumnSpanned(True) self.setItemWidget(wi, 1, f) return wi def _make_override_item(self, fqn, path): items = self._make_param_items( fqn, path, False, self.indexOfTopLevelItem(self._override_prompt_item)) self._override_items[(fqn, path)] = items self._set_save_timer() # Make sure layout is updated to accomodate new row; without this, the # new item and the add prompt button overlap on Qt 5.6.2/Win64 until # the dock is resized for the first time. geom = self.geometry() self.resize(geom.width(), geom.height()) def _make_add_override_prompt_item(self): self._override_prompt_item = QtWidgets.QTreeWidgetItem() self.addTopLevelItem(self._override_prompt_item) # Layout to display button/prompt label, depending on which one is active. left = LayoutWidget() self._add_override_button = QtWidgets.QToolButton() self._add_override_button.setIcon(self._add_override_icon) self._add_override_button.clicked.connect(self._set_override_line_active) self._add_override_button.setShortcut(QtCore.Qt.CTRL + QtCore.Qt.Key_T) left.addWidget(self._add_override_button, 0, 0) self._add_override_prompt_label = QtWidgets.QLabel("Add parameter:") left.addWidget(self._add_override_prompt_label, 0, 0) left.layout.setColumnStretch(0, 0) left.layout.setColumnStretch(1, 1) self.setItemWidget(self._override_prompt_item, 0, left) prompt = LayoutWidget() self._add_override_prompt_box = FuzzySelectWidget([]) self._add_override_prompt_box.finished.connect( lambda a: self._make_override_item(*self._param_choice_map[a])) self._add_override_prompt_box.aborted.connect(self._set_override_line_idle) prompt.addWidget(self._add_override_prompt_box) self.setItemWidget(self._override_prompt_item, 1, prompt) def _set_override_line_idle(self): self._add_override_button.setEnabled(True) self._add_override_button.setVisible(True) self._add_override_prompt_label.setVisible(False) self._add_override_prompt_box.setVisible(False) def _set_override_line_active(self): self._update_param_choice_map() self._add_override_prompt_box.set_choices([ (s, 0) for s in self._param_choice_map.keys() ]) self._add_override_button.setEnabled(False) self._add_override_button.setVisible(False) self._add_override_prompt_label.setVisible(True) self._add_override_prompt_box.setVisible(True) # TODO: See whether I can't get focus proxies to work. self._add_override_prompt_box.line_edit.setFocus() def _make_group_header_item(self, name): group = QtWidgets.QTreeWidgetItem([name]) 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) return group def _ensure_group_widget(self, name): if name in self._groups: return self._groups[name] group = self._make_group_header_item(name) if self.override_separator: self.insertTopLevelItem(self.indexOfTopLevelItem(self.override_separator), group) else: self.addTopLevelItem(group) self._groups[name] = group return group def _recompute_vanilla_argument_clicked(self, name): asyncio.ensure_future(self._recompute_vanilla_argument(name)) async def _recompute_vanilla_argument(self, name): try: arginfo, _ = await self.manager.examine_arginfo(self.expurl) except Exception: logger.error("Could not recompute argument '%s' of '%s'", name, self.expurl, exc_info=True) return argument = self.manager.get_submission_arguments(self.expurl)[name] procdesc = arginfo[name][0] state = procdesc_to_entry(procdesc).default_state(procdesc) argument["desc"] = procdesc argument["state"] = state widgets = self._arg_to_widgets[name] widgets["entry"].deleteLater() widgets["entry"] = procdesc_to_entry(procdesc)(argument) 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_to_default(self, fqn, path): self._param_entries[(fqn, path)].read_from_params({}, self.manager.datasets) def _remove_override(self, fqn, path): items = self._override_items[(fqn, path)] for item in items: idx = self.indexOfTopLevelItem(item) self.takeTopLevelItem(idx) del self._param_entries[(fqn, path)] del self._override_items[(fqn, path)] self._set_save_timer() def _update_param_choice_map(self): self._param_choice_map = dict() def add(fqn, path): # Skip params already displayed. if (fqn, path) in self._param_entries: return schema = self._schema_for_fqn(fqn) display_string = "{} – {}".format(self._param_display_name(fqn, path), schema["description"]) self._param_choice_map[display_string] = (fqn, path) fqn_occurences = Counter() for path, fqns in self._ndscan_params["instances"].items(): for fqn in fqns: add(fqn, path) fqn_occurences[fqn] += 1 # TODO: Offer non-global wildcards for parameters used in multiple hierarchies. for fqn, count in fqn_occurences.items(): if count > 1: add(fqn, "*") def _build_shortened_fqns(self): self.shortened_fqns = shorten_to_unambiguous_suffixes( self._ndscan_params["schemata"].keys(), lambda fqn, n: ".".join(fqn.split(".")[-(n + 1):])) def _param_display_name(self, fqn, path): if not path: path = "/" return self.shortened_fqns[fqn] + "@" + path def _schema_for_fqn(self, fqn): return self._ndscan_params["schemata"][fqn] def _set_save_timer(self): self._save_timer.start(500) def _save_to_argument(self): # Stop timer if it is still running. self._save_timer.stop() # Reset previous overrides/scan axes, repopulate with currently active ones. self._ndscan_params.setdefault("scan", {})["axes"] = [] self._ndscan_params["overrides"] = {} for item in self._param_entries.values(): item.write_to_params(self._ndscan_params) if self.scan_options is None: # Not actually a scannable experiment – delete the scan metadata key, which # we've set above to keep code straightforward. del self._ndscan_params["scan"] else: # Store scan parameters. self.scan_options.write_to_params(self._ndscan_params) _update_ndscan_params(self._arguments, self._ndscan_params) def _make_override_entry(self, fqn, path): schema = self._schema_for_fqn(fqn) entry_class = FloatOverrideEntry if schema["type"] == "string": entry_class = StringOverrideEntry # TODO: Properly handle int, add errors (or default to PYON value). is_scannable = ((self.scan_options is not None) and schema.get("spec", {}).get("is_scannable", True)) return entry_class(schema, path, is_scannable, self._randomise_scan_icon)