diff --git a/artiq/dashboard/experiments.py b/artiq/dashboard/experiments.py index bbc46040e0..e66f75d516 100644 --- a/artiq/dashboard/experiments.py +++ b/artiq/dashboard/experiments.py @@ -11,7 +11,7 @@ from artiq.gui.entries import procdesc_to_entry, EntryTreeWidget from artiq.gui.fuzzy_select import FuzzySelectWidget -from artiq.gui.tools import (LayoutWidget, log_level_to_name, get_open_file_name) +from artiq.gui.tools import (log_level_to_name, get_open_file_name) from artiq.tools import parse_devarg_override, unparse_devarg_override @@ -47,20 +47,16 @@ def __init__(self, manager, dock, expurl): QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_BrowserReload)) recompute_arguments.clicked.connect(dock._recompute_arguments_clicked) + recompute_arguments.setMinimumWidth(200) load_hdf5 = QtWidgets.QPushButton("Load HDF5") load_hdf5.setIcon(QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_DialogOpenButton)) load_hdf5.clicked.connect(dock._load_hdf5_clicked) + load_hdf5.setMinimumWidth(120) - buttons = LayoutWidget() - buttons.addWidget(recompute_arguments, 1, 1) - buttons.addWidget(load_hdf5, 1, 2) - buttons.layout.setColumnStretch(0, 1) - buttons.layout.setColumnStretch(1, 0) - buttons.layout.setColumnStretch(2, 0) - buttons.layout.setColumnStretch(3, 1) - self.setItemWidget(self.bottom_item, 1, buttons) + self.setItemWidget(self.bottom_item, 0, load_hdf5) + self.setItemWidget(self.bottom_item, 1, recompute_arguments) def reset_entry(self, key): asyncio.ensure_future(self._recompute_argument(key)) @@ -115,47 +111,113 @@ def drawComplexControl(self, control, option, painter, widget=None): class _ExperimentDock(QtWidgets.QMdiSubWindow): sigClosed = QtCore.pyqtSignal() + def __init__(self, manager, expurl): - QtWidgets.QMdiSubWindow.__init__(self) + super(_ExperimentDock, self).__init__() qfm = QtGui.QFontMetrics(self.font()) self.resize(100 * qfm.averageCharWidth(), 30 * qfm.lineSpacing()) self.setWindowTitle(expurl) self.setWindowIcon(QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_FileDialogContentsView)) - self.layout = QtWidgets.QGridLayout() + self.manager = manager + self.expurl = expurl + self.scheduling = manager.get_submission_scheduling(expurl) + self.options = manager.get_submission_options(expurl) + self.hdf5_load_directory = os.path.expanduser("~") + + self.master_layout = QtWidgets.QVBoxLayout() + self._create_argument_editor() + self.master_layout.addWidget(self.argeditor) + + # Create a toggle button that will collapse/expand the options. + self.fold_toggle = QtWidgets.QToolButton(text="Collapse scheduler settings", + checkable=True) + self.fold_toggle.setToolButtonStyle( + QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.fold_toggle.setChecked(False) + self.fold_toggle.setToolTip("Collapse/Expand scheduler settings") + self.fold_toggle.setArrowType(QtCore.Qt.ArrowType.DownArrow) + self.fold_toggle.clicked.connect(self.on_fold_toggle) + self.master_layout.addWidget(self.fold_toggle) + + # Create a container widget (with a grid layout) for all the foldable + # options. + self.foldable_container = QtWidgets.QWidget() + self.foldable_layout = QtWidgets.QGridLayout() + self.foldable_layout.setSpacing(5) + self.foldable_layout.setContentsMargins(5, 5, 5, 5) + self.foldable_container.setLayout(self.foldable_layout) + self.master_layout.addWidget(self.foldable_container) + + # Create a container widget (with a horizontal layout) for + # always-visible buttons. + self.always_visible_container = QtWidgets.QWidget() + self.always_visible_layout = QtWidgets.QHBoxLayout() + self.always_visible_layout.setSpacing(5) + self.always_visible_layout.setContentsMargins(5, 5, 5, 5) + self.always_visible_container.setLayout(self.always_visible_layout) + self.master_layout.addWidget(self.always_visible_container) + + # Set the master layout on the top widget. top_widget = QtWidgets.QWidget() - top_widget.setLayout(self.layout) + top_widget.setLayout(self.master_layout) self.setWidget(top_widget) - self.layout.setSpacing(5) - self.layout.setContentsMargins(5, 5, 5, 5) - self.manager = manager - self.expurl = expurl + # --- Create the various widget groups --- + # Place all “foldable” widgets into the foldable_layout. + self._create_due_date_widgets() + self._create_pipeline_widgets() + self._create_priority_widgets() + self._create_flush_widgets() + self._create_devarg_override_widgets() + self._create_log_level_widgets() + self._create_repo_rev_widgets() + # Place the submit and termination buttons into the always-visible + # container. + self._create_submit_widgets() + self._create_reqterm_widgets() + + self.on_fold_toggle() + + def on_fold_toggle(self): + """Toggle the visibility of the options.""" + if self.fold_toggle.isChecked(): + self.foldable_container.show() + self.fold_toggle.setText("Collapse scheduler settings") + self.fold_toggle.setArrowType(QtCore.Qt.ArrowType.DownArrow) + if self.width() < 550: + ideal_width = self.sizeHint().width() + self.resize(ideal_width, self.height()) + if self.height() < 285: + self.resize(self.width(), 285) - editor_class = self.manager.get_argument_editor_class(expurl) - self.argeditor = editor_class(self.manager, self, self.expurl) - self.layout.addWidget(self.argeditor, 0, 0, 1, 5) - self.layout.setRowStretch(0, 1) + else: + self.foldable_container.hide() + self.fold_toggle.setText("Expand scheduler settings") + self.fold_toggle.setArrowType(QtCore.Qt.ArrowType.RightArrow) + self.always_visible_container.setMaximumHeight(30) - scheduling = manager.get_submission_scheduling(expurl) - options = manager.get_submission_options(expurl) + def _create_argument_editor(self): + editor_class = self.manager.get_argument_editor_class(self.expurl) + self.argeditor = editor_class(self.manager, self, self.expurl) + def _create_due_date_widgets(self): datetime = QtWidgets.QDateTimeEdit() datetime.setDisplayFormat("MMM d yyyy hh:mm:ss") datetime_en = QtWidgets.QCheckBox("Due date:") - self.layout.addWidget(datetime_en, 1, 0) - self.layout.addWidget(datetime, 1, 1) + self.foldable_layout.addWidget(datetime_en, 1, 0) + self.foldable_layout.addWidget(datetime, 1, 1) - if scheduling["due_date"] is None: + if self.scheduling["due_date"] is None: datetime.setDate(QtCore.QDate.currentDate()) else: datetime.setDateTime(QtCore.QDateTime.fromMSecsSinceEpoch( - int(scheduling["due_date"] * 1000))) - datetime_en.setChecked(scheduling["due_date"] is not None) + int(self.scheduling["due_date"] * 1000))) + datetime_en.setChecked(self.scheduling["due_date"] is not None) def update_datetime(dt): - scheduling["due_date"] = dt.toMSecsSinceEpoch() / 1000 + self.scheduling["due_date"] = dt.toMSecsSinceEpoch() / 1000 datetime_en.setChecked(True) datetime.dateTimeChanged.connect(update_datetime) @@ -164,134 +226,139 @@ def update_datetime_en(checked): due_date = datetime.dateTime().toMSecsSinceEpoch() / 1000 else: due_date = None - scheduling["due_date"] = due_date + self.scheduling["due_date"] = due_date datetime_en.stateChanged.connect(update_datetime_en) + def _create_pipeline_widgets(self): self.pipeline_name = QtWidgets.QLineEdit() - pipeline_name = self.pipeline_name - self.layout.addWidget(QtWidgets.QLabel("Pipeline:"), 1, 2) - self.layout.addWidget(pipeline_name, 1, 3) + self.foldable_layout.addWidget(QtWidgets.QLabel("Pipeline:"), 1, 2) + self.foldable_layout.addWidget(self.pipeline_name, 1, 3) - pipeline_name.setText(scheduling["pipeline_name"]) + self.pipeline_name.setText(self.scheduling["pipeline_name"]) def update_pipeline_name(text): - scheduling["pipeline_name"] = text - pipeline_name.textChanged.connect(update_pipeline_name) + self.scheduling["pipeline_name"] = text + self.pipeline_name.textChanged.connect(update_pipeline_name) + def _create_priority_widgets(self): self.priority = QtWidgets.QSpinBox() - priority = self.priority - priority.setRange(-99, 99) - self.layout.addWidget(QtWidgets.QLabel("Priority:"), 2, 0) - self.layout.addWidget(priority, 2, 1) - - priority.setValue(scheduling["priority"]) + self.priority.setRange(-99, 99) + self.foldable_layout.addWidget(QtWidgets.QLabel("Priority:"), 2, 0) + self.foldable_layout.addWidget(self.priority, 2, 1) + self.priority.setValue(self.scheduling["priority"]) def update_priority(value): - scheduling["priority"] = value - priority.valueChanged.connect(update_priority) + self.scheduling["priority"] = value + self.priority.valueChanged.connect(update_priority) + def _create_flush_widgets(self): self.flush = QtWidgets.QCheckBox("Flush") - flush = self.flush - flush.setToolTip("Flush the pipeline (of current- and higher-priority " - "experiments) before starting the experiment") - self.layout.addWidget(flush, 2, 2) + self.flush.setToolTip("Flush the pipeline (of current- and higher-priority " + "experiments) before starting the experiment") + self.foldable_layout.addWidget(self.flush, 2, 2) - flush.setChecked(scheduling["flush"]) + self.flush.setChecked(self.scheduling["flush"]) - def update_flush(checked): - scheduling["flush"] = bool(checked) - flush.stateChanged.connect(update_flush) + def update_flush(state): + self.scheduling["flush"] = bool(state) + self.flush.stateChanged.connect(update_flush) - devarg_override = QtWidgets.QComboBox() - devarg_override.setEditable(True) - devarg_override.lineEdit().setPlaceholderText("Override device arguments") - devarg_override.lineEdit().setClearButtonEnabled(True) - devarg_override.insertItem(0, "core:analyze_at_run_end=True") - devarg_override.insertItem(1, "core:report_invariants=True") - self.layout.addWidget(devarg_override, 2, 3) + def _create_devarg_override_widgets(self): + self.devarg_override = QtWidgets.QComboBox() + self.devarg_override.setEditable(True) + self.devarg_override.lineEdit().setPlaceholderText( + "Override device arguments") + self.devarg_override.lineEdit().setClearButtonEnabled(True) + self.devarg_override.insertItem(0, "core:analyze_at_run_end=True") + self.devarg_override.insertItem(1, "core:report_invariants=True") + self.foldable_layout.addWidget(self.devarg_override, 2, 3) - devarg_override.setCurrentText(options["devarg_override"]) + self.devarg_override.setCurrentText(self.options["devarg_override"]) def update_devarg_override(text): - options["devarg_override"] = text - devarg_override.editTextChanged.connect(update_devarg_override) - self.devarg_override = devarg_override - - log_level = QtWidgets.QComboBox() - log_level.addItems(log_levels) - log_level.setCurrentIndex(1) - log_level.setToolTip("Minimum level for log entry production") - log_level_label = QtWidgets.QLabel("Logging level:") - log_level_label.setToolTip("Minimum level for log message production") - self.layout.addWidget(log_level_label, 3, 0) - self.layout.addWidget(log_level, 3, 1) - - log_level.setCurrentIndex(log_levels.index( - log_level_to_name(options["log_level"]))) + self.options["devarg_override"] = text + self.devarg_override.editTextChanged.connect(update_devarg_override) + + def _create_log_level_widgets(self): + self.log_level = QtWidgets.QComboBox() + self.log_level.addItems(log_levels) + self.log_level.setCurrentIndex(1) + self.log_level.setToolTip("Minimum level for log entry production") + self.log_level_label = QtWidgets.QLabel("Logging level:") + self.log_level_label.setToolTip( + "Minimum level for log message production") + self.foldable_layout.addWidget(self.log_level_label, 3, 0) + self.foldable_layout.addWidget(self.log_level, 3, 1) + + self.log_level.setCurrentIndex(log_levels.index( + log_level_to_name(self.options["log_level"]))) def update_log_level(index): - options["log_level"] = getattr(logging, log_level.currentText()) - log_level.currentIndexChanged.connect(update_log_level) - self.log_level = log_level - - if "repo_rev" in options: - repo_rev = QtWidgets.QLineEdit() - repo_rev.setPlaceholderText("current") - repo_rev.setClearButtonEnabled(True) - repo_rev_label = QtWidgets.QLabel("Rev / ref:") - repo_rev_label.setToolTip("Experiment repository revision " - "(commit ID) or reference (branch " - "or tag) to use") - self.layout.addWidget(repo_rev_label, 3, 2) - self.layout.addWidget(repo_rev, 3, 3) - - if options["repo_rev"] is not None: - repo_rev.setText(options["repo_rev"]) + self.options["log_level"] = getattr(logging, self.log_level.currentText()) + self.log_level.currentIndexChanged.connect(update_log_level) + + def _create_repo_rev_widgets(self): + if "repo_rev" in self.options: + self.repo_rev = QtWidgets.QLineEdit() + self.repo_rev.setPlaceholderText("current") + self.repo_rev.setClearButtonEnabled(True) + self.repo_rev_label = QtWidgets.QLabel("Rev / ref:") + self.repo_rev_label.setToolTip("Experiment repository revision " + "(commit ID) or reference (branch " + "or tag) to use") + self.foldable_layout.addWidget(self.repo_rev_label, 3, 2) + self.foldable_layout.addWidget(self.repo_rev, 3, 3) + + if self.options["repo_rev"] is not None: + self.repo_rev.setText(self.options["repo_rev"]) def update_repo_rev(text): if text: - options["repo_rev"] = text + self.options["repo_rev"] = text else: - options["repo_rev"] = None - repo_rev.textChanged.connect(update_repo_rev) - self.repo_rev = repo_rev + self.options["repo_rev"] = None + self.repo_rev.textChanged.connect(update_repo_rev) - submit = QtWidgets.QPushButton("Submit") - submit.setIcon(QtWidgets.QApplication.style().standardIcon( + def _create_submit_widgets(self): + self.submit = QtWidgets.QPushButton("Submit") + self.submit.setIcon(QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_DialogOkButton)) - submit.setToolTip("Schedule the experiment (Ctrl+Return)") - submit.setShortcut("CTRL+RETURN") - submit.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding) - self.layout.addWidget(submit, 1, 4, 2, 1) - submit.clicked.connect(self.submit_clicked) - - reqterm = QtWidgets.QPushButton("Terminate instances") - reqterm.setIcon(QtWidgets.QApplication.style().standardIcon( - QtWidgets.QStyle.StandardPixmap.SP_DialogCancelButton)) - reqterm.setToolTip("Request termination of instances (Ctrl+Backspace)") - reqterm.setShortcut("CTRL+BACKSPACE") - reqterm.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding) - self.layout.addWidget(reqterm, 3, 4) - reqterm.clicked.connect(self.reqterm_clicked) - - self.hdf5_load_directory = os.path.expanduser("~") + self.submit.setToolTip("Schedule the experiment (Ctrl+Return)") + self.submit.setShortcut("CTRL+RETURN") + self.submit.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding) + self.submit.setMaximumHeight(25) + self.submit.setMinimumWidth(175) + self.always_visible_layout.addWidget(self.submit) + self.submit.clicked.connect(self.submit_clicked) def submit_clicked(self): self.argeditor.about_to_submit() try: self.manager.submit(self.expurl) - except: + except Exception: # May happen when experiment has been removed # from repository/explist logger.error("Failed to submit '%s'", self.expurl, exc_info=True) + def _create_reqterm_widgets(self): + self.reqterm = QtWidgets.QPushButton("Terminate instances") + self.reqterm.setIcon(QtWidgets.QApplication.style().standardIcon( + QtWidgets.QStyle.StandardPixmap.SP_DialogCancelButton)) + self.reqterm.setToolTip("Request termination of instances (Ctrl+Backspace)") + self.reqterm.setShortcut("CTRL+BACKSPACE") + self.reqterm.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding) + self.reqterm.setMaximumHeight(25) + self.reqterm.setMinimumWidth(175) + self.always_visible_layout.addWidget(self.reqterm) + self.reqterm.clicked.connect(self.reqterm_clicked) + def reqterm_clicked(self): try: self.manager.request_inst_term(self.expurl) - except: + except Exception: # May happen when experiment has been removed # from repository/explist logger.error("Failed to request termination of instances of '%s'", @@ -321,7 +388,7 @@ async def _recompute_arguments_task(self, overrides=dict()): editor_class = self.manager.get_argument_editor_class(self.expurl) self.argeditor = editor_class(self.manager, self, self.expurl) - self.layout.addWidget(self.argeditor, 0, 0, 1, 5) + self.master_layout.insertWidget(0, self.argeditor) self.argeditor.restore_state(argeditor_state) self.apply_window_color() @@ -395,11 +462,11 @@ async def _recompute_sched_options_task(self): return sched_defaults = expdesc["scheduler_defaults"] - scheduling = self.manager.get_submission_scheduling(self.expurl) - scheduling.update(sched_defaults) - self.priority.setValue(scheduling["priority"]) - self.pipeline_name.setText(scheduling["pipeline_name"]) - self.flush.setChecked(scheduling["flush"]) + self.scheduling = self.manager.get_submission_scheduling(self.expurl) + self.scheduling.update(sched_defaults) + self.priority.setValue(self.scheduling["priority"]) + self.pipeline_name.setText(self.scheduling["pipeline_name"]) + self.flush.setChecked(self.scheduling["flush"]) def _load_hdf5_clicked(self): asyncio.ensure_future(self._load_hdf5_task()) @@ -450,13 +517,17 @@ def save_state(self): return { "args": self.argeditor.save_state(), "geometry": bytes(self.saveGeometry()), - "hdf5_load_directory": self.hdf5_load_directory + "hdf5_load_directory": self.hdf5_load_directory, + "options_expanded": self.fold_toggle.isChecked() } def restore_state(self, state): self.argeditor.restore_state(state["args"]) self.restoreGeometry(QtCore.QByteArray(state["geometry"])) self.hdf5_load_directory = state["hdf5_load_directory"] + if "options_expanded" in state: # Only useful for the upgrade + self.fold_toggle.setChecked(state["options_expanded"]) + self.on_fold_toggle() class _QuickOpenDialog(QtWidgets.QDialog): diff --git a/artiq/gui/entries.py b/artiq/gui/entries.py index 1283f001ba..aac24dee93 100644 --- a/artiq/gui/entries.py +++ b/artiq/gui/entries.py @@ -23,7 +23,7 @@ def __init__(self): set_resize_mode = self.header().setSectionResizeMode else: set_resize_mode = self.header().setResizeMode - set_resize_mode(0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) + set_resize_mode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) set_resize_mode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) set_resize_mode(2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) self.header().setVisible(False)