diff --git a/artiq/dashboard/experiments.py b/artiq/dashboard/experiments.py index 1986a4a83..3c9d5edac 100644 --- a/artiq/dashboard/experiments.py +++ b/artiq/dashboard/experiments.py @@ -164,7 +164,7 @@ class _ArgumentEditor(QtWidgets.QTreeWidget): async def _recompute_argument(self, name): try: - expdesc = await self.manager.compute_expdesc(self.expurl) + expdesc, _ = await self.manager.compute_expdesc(self.expurl) except: logger.error("Could not recompute argument '%s' of '%s'", name, self.expurl, exc_info=True) @@ -250,7 +250,8 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow): self.manager = manager self.expurl = expurl - self.argeditor = _ArgumentEditor(self.manager, self, self.expurl) + 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) @@ -401,7 +402,7 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow): async def _recompute_arguments_task(self, overrides=dict()): try: - expdesc = await self.manager.compute_expdesc(self.expurl) + expdesc, ui_name = await self.manager.compute_expdesc(self.expurl) except: logger.error("Could not recompute experiment description of '%s'", self.expurl, exc_info=True) @@ -414,12 +415,13 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow): arginfo[k][0]["default"].insert(0, v) else: arginfo[k][0]["default"] = v - self.manager.initialize_submission_arguments(self.expurl, arginfo) + self.manager.initialize_submission_arguments(self.expurl, arginfo, ui_name) argeditor_state = self.argeditor.save_state() self.argeditor.deleteLater() - self.argeditor = _ArgumentEditor(self.manager, self, self.expurl) + editor_class = self.manager.get_argument_editor_class(self.expurl) + self.argeditor = editor_class(self.manager, self, self.expurl) self.argeditor.restore_state(argeditor_state) self.layout.addWidget(self.argeditor, 0, 0, 1, 5) @@ -432,7 +434,7 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow): async def _recompute_sched_options_task(self): try: - expdesc = await self.manager.compute_expdesc(self.expurl) + expdesc, _ = await self.manager.compute_expdesc(self.expurl) except: logger.error("Could not recompute experiment description of '%s'", self.expurl, exc_info=True) @@ -555,6 +557,12 @@ class _QuickOpenDialog(QtWidgets.QDialog): class ExperimentManager: + #: Global registry for custom argument editor classes, indexed by the experiment + #: `argument_ui` string; can be populated by dashboard plugins such as ndscan. + #: If no handler for a requested UI name is found, the default built-in argument + #: editor will be used. + argument_ui_classes = dict() + def __init__(self, main_window, dataset_sub, explist_sub, schedule_sub, schedule_ctl, experiment_db_ctl): @@ -566,6 +574,7 @@ class ExperimentManager: self.submission_scheduling = dict() self.submission_options = dict() self.submission_arguments = dict() + self.argument_ui_names = dict() self.datasets = dict() dataset_sub.add_setmodel_callback(self.set_dataset_model) @@ -602,6 +611,17 @@ class ExperimentManager: else: raise ValueError("Malformed experiment URL") + def get_argument_editor_class(self, expurl): + ui_name = self.argument_ui_names.get(expurl, None) + if not ui_name and expurl[:5] == "repo:": + ui_name = self.explist.get(expurl[5:], {}).get("argument_ui", None) + if ui_name: + result = self.argument_ui_classes.get(ui_name, None) + if result: + return result + logger.warning("Ignoring unknown argument UI '%s'", ui_name) + return _ArgumentEditor + def get_submission_scheduling(self, expurl): if expurl in self.submission_scheduling: return self.submission_scheduling[expurl] @@ -631,7 +651,7 @@ class ExperimentManager: self.submission_options[expurl] = options return options - def initialize_submission_arguments(self, expurl, arginfo): + def initialize_submission_arguments(self, expurl, arginfo, ui_name): arguments = OrderedDict() for name, (procdesc, group, tooltip) in arginfo.items(): state = procdesc_to_entry(procdesc).default_state(procdesc) @@ -642,6 +662,7 @@ class ExperimentManager: "state": state, # mutated by entries } self.submission_arguments[expurl] = arguments + self.argument_ui_names[expurl] = ui_name return arguments def get_submission_arguments(self, expurl): @@ -651,9 +672,9 @@ class ExperimentManager: if expurl[:5] != "repo:": raise ValueError("Submission arguments must be preinitialized " "when not using repository") - arginfo = self.explist[expurl[5:]]["arginfo"] - arguments = self.initialize_submission_arguments(expurl, arginfo) - return arguments + class_desc = self.explist[expurl[5:]] + return self.initialize_submission_arguments(expurl, + class_desc["arginfo"], class_desc.get("argument_ui", None)) def open_experiment(self, expurl): if expurl in self.open_experiments: @@ -755,13 +776,15 @@ class ExperimentManager: revision = None description = await self.experiment_db_ctl.examine( file, use_repository, revision) - return description[class_name] + class_desc = description[class_name] + return class_desc, class_desc.get("argument_ui", None) async def open_file(self, file): description = await self.experiment_db_ctl.examine(file, False) for class_name, class_desc in description.items(): expurl = "file:{}@{}".format(class_name, file) - self.initialize_submission_arguments(expurl, class_desc["arginfo"]) + self.initialize_submission_arguments(expurl, class_desc["arginfo"], + class_desc.get("argument_ui", None)) if expurl in self.open_experiments: self.open_experiments[expurl].close() self.open_experiment(expurl) @@ -774,6 +797,7 @@ class ExperimentManager: "options": self.submission_options, "arguments": self.submission_arguments, "docks": self.dock_states, + "argument_uis": self.argument_ui_names, "open_docks": set(self.open_experiments.keys()) } @@ -784,6 +808,7 @@ class ExperimentManager: self.submission_scheduling = state["scheduling"] self.submission_options = state["options"] self.submission_arguments = state["arguments"] + self.argument_ui_names = state.get("argument_uis", {}) for expurl in state["open_docks"]: self.open_experiment(expurl) diff --git a/artiq/master/experiments.py b/artiq/master/experiments.py index 918c92dc8..098ddc334 100644 --- a/artiq/master/experiments.py +++ b/artiq/master/experiments.py @@ -30,7 +30,6 @@ class _RepoScanner: raise for class_name, class_desc in description.items(): name = class_desc["name"] - arginfo = class_desc["arginfo"] if "/" in name: logger.warning("Character '/' is not allowed in experiment " "name (%s)", name) @@ -47,7 +46,8 @@ class _RepoScanner: entry = { "file": filename, "class_name": class_name, - "arginfo": arginfo, + "arginfo": class_desc["arginfo"], + "argument_ui": class_desc["argument_ui"], "scheduler_defaults": class_desc["scheduler_defaults"] } entry_dict[name] = entry diff --git a/artiq/master/worker_impl.py b/artiq/master/worker_impl.py index 8d4ee9e36..97589dc14 100644 --- a/artiq/master/worker_impl.py +++ b/artiq/master/worker_impl.py @@ -205,7 +205,10 @@ def examine(device_mgr, dataset_mgr, file): (k, (proc.describe(), group, tooltip)) for k, (proc, group, tooltip) in argument_mgr.requested_args.items() ) - register_experiment(class_name, name, arginfo, scheduler_defaults) + argument_ui = None + if hasattr(exp_class, "argument_ui"): + argument_ui = exp_class.argument_ui + register_experiment(class_name, name, arginfo, argument_ui, scheduler_defaults) finally: new_keys = set(sys.modules.keys()) for key in new_keys - previous_keys: