Esempio n. 1
0
    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)
Esempio n. 2
0
    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)
Esempio n. 3
0
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()
Esempio n. 4
0
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)