diff --git a/artiq/dashboard/experiments.py b/artiq/dashboard/experiments.py index 4be556d79..2ea64964f 100644 --- a/artiq/dashboard/experiments.py +++ b/artiq/dashboard/experiments.py @@ -28,6 +28,7 @@ class _ArgumentEditor(EntryTreeWidget): def __init__(self, manager, dock, expurl): self.manager = manager self.expurl = expurl + self.dock = dock EntryTreeWidget.__init__(self) @@ -78,6 +79,26 @@ async def _recompute_argument(self, name): argument["desc"] = procdesc argument["state"] = state self.update_argument(name, argument) + self.reapply_color() + + def reapply_color(self): + colors = self.dock.get_themed_colors() + palette = self.dock.palette() + self.apply_color(palette, colors["window"]) + + def apply_color(self, palette, color): + self.setPalette(palette) + for child in self.findChildren(QtWidgets.QWidget): + child.setPalette(palette) + child.setAutoFillBackground(True) + for i in range(self.topLevelItemCount()): + self._set_item_color(self.topLevelItem(i), color) + + def _set_item_color(self, item, color): + for column in range(item.columnCount()): + item.setBackground(column, color) + for child_index in range(item.childCount()): + self._set_item_color(item.child(child_index), color) # Hooks that allow user-supplied argument editors to react to imminent user # actions. Here, we always keep the manager-stored submission arguments @@ -92,6 +113,20 @@ def about_to_close(self): log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +class _ThemedTitleBar(QtWidgets.QProxyStyle): + def __init__(self, dock, style=None): + super().__init__(style) + self.dock = dock + + def drawComplexControl(self, control, option, painter, widget=None): + if control == QtWidgets.QStyle.ComplexControl.CC_TitleBar: + option = QtWidgets.QStyleOptionTitleBar(option) + colors = self.dock.get_themed_colors() + option.palette.setColor(QtGui.QPalette.ColorRole.Window, colors["title_bar"]) + option.palette.setColor(QtGui.QPalette.ColorRole.Highlight, colors["title_bar"]) + self.baseStyle().drawComplexControl(control, option, painter, widget) + + class _ExperimentDock(QtWidgets.QMdiSubWindow): sigClosed = QtCore.pyqtSignal() @@ -303,14 +338,63 @@ async def _recompute_arguments_task(self, overrides=dict()): self.argeditor = editor_class(self.manager, self, self.expurl) self.layout.addWidget(self.argeditor, 0, 0, 1, 5) self.argeditor.restore_state(argeditor_state) + self.argeditor.reapply_color() def contextMenuEvent(self, event): menu = QtWidgets.QMenu(self) + select_title_bar_color = menu.addAction("Select title bar color") + select_window_color = menu.addAction("Select window color") + reset_colors = menu.addAction("Reset to default colors") + menu.addSeparator() reset_sched = menu.addAction("Reset scheduler settings") action = menu.exec(self.mapToGlobal(event.pos())) - if action == reset_sched: + if action == select_title_bar_color: + self.select_color("title_bar") + elif action == select_window_color: + self.select_color("window") + elif action == reset_colors: + self.reset_colors() + elif action == reset_sched: asyncio.ensure_future(self._recompute_sched_options_task()) + def select_color(self, key): + color = QtWidgets.QColorDialog.getColor( + title=f"Select {key.replace('_', ' ').title()} Color") + if color.isValid(): + self.manager.set_color(self.expurl, key, color.name()) + self.apply_colors() + + def apply_colors(self): + colors = self.get_themed_colors() + palette = self.modify_palette(colors) + self.setPalette(palette) + self.setStyle(_ThemedTitleBar(self)) + self.argeditor.apply_color(palette, colors["window"]) + + def modify_palette(self, colors): + palette = self.palette() + palette.setColor(QtGui.QPalette.ColorRole.Window, colors["window"]) + palette.setColor(QtGui.QPalette.ColorRole.Base, colors["window"]) + palette.setColor(QtGui.QPalette.ColorRole.Button, colors["window"]) + palette.setColor(QtGui.QPalette.ColorRole.Text, colors["window_text"]) + palette.setColor(QtGui.QPalette.ColorRole.ButtonText, colors["window_text"]) + palette.setColor(QtGui.QPalette.ColorRole.WindowText, colors["window_text"]) + return palette + + def get_themed_colors(self): + colors = self.manager.get_colors(self.expurl) + themed_colors = { + "window": QtGui.QColor(colors["window"]), + "title_bar": QtGui.QColor(colors["title_bar"]) + } + themed_colors["window_text"] = QtGui.QColor( + "#000000" if themed_colors["window"].lightness() > 128 else "#FFFFFF") + return themed_colors + + def reset_colors(self): + self.manager.reset_colors(self.expurl) + self.apply_colors() + async def _recompute_sched_options_task(self): try: expdesc, _ = await self.manager.compute_expdesc(self.expurl) @@ -457,6 +541,7 @@ def __init__(self, main_window, dataset_sub, self.submission_options = dict() self.submission_arguments = dict() self.argument_ui_names = dict() + self.colors = dict() self.datasets = dict() dataset_sub.add_setmodel_callback(self.set_dataset_model) @@ -483,6 +568,27 @@ def set_explist_model(self, model): def set_schedule_model(self, model): self.schedule = model.backing_store + def set_color(self, expurl, key, value): + if expurl not in self.colors: + self.colors[expurl] = {} + self.colors[expurl][key] = value + + def get_colors(self, expurl): + if expurl in self.colors: + return self.colors[expurl] + else: + palette = QtWidgets.QApplication.palette() + colors = { + "window": palette.color(QtGui.QPalette.ColorRole.Window).name(), + "title_bar": palette.color(QtGui.QPalette.ColorRole.Highlight).name() + } + self.colors[expurl] = colors + return colors + + def reset_colors(self, expurl): + if expurl in self.colors: + del self.colors[expurl] + def resolve_expurl(self, expurl): if expurl[:5] == "repo:": expinfo = self.explist[expurl[5:]] @@ -592,6 +698,7 @@ def open_experiment(self, expurl): self.open_experiments[expurl] = dock dock.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) self.main_window.centralWidget().addSubWindow(dock) + dock.apply_colors() dock.show() dock.sigClosed.connect(partial(self.on_dock_closed, expurl)) if expurl in self.dock_states: @@ -708,7 +815,8 @@ def save_state(self): "arguments": self.submission_arguments, "docks": self.dock_states, "argument_uis": self.argument_ui_names, - "open_docks": set(self.open_experiments.keys()) + "open_docks": set(self.open_experiments.keys()), + "colors": self.colors } def restore_state(self, state): @@ -719,6 +827,7 @@ def restore_state(self, state): self.submission_options = state["options"] self.submission_arguments = state["arguments"] self.argument_ui_names = state.get("argument_uis", {}) + self.colors = state.get("colors", {}) for expurl in state["open_docks"]: self.open_experiment(expurl)