diff --git a/artiq/dashboard/experiments.py b/artiq/dashboard/experiments.py index 4be556d79..770ac871d 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_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 col in range(item.columnCount()): + item.setBackground(col, QtGui.QBrush() if color is None else QtGui.QColor(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,22 @@ def about_to_close(self): log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +class CustomTitleSyle(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_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) + return + self.baseStyle().drawComplexControl(control, option, painter, widget) + + class _ExperimentDock(QtWidgets.QMdiSubWindow): sigClosed = QtCore.pyqtSignal() @@ -102,6 +139,7 @@ def __init__(self, manager, expurl): self.setWindowTitle(expurl) self.setWindowIcon(QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_FileDialogContentsView)) + self.style = None self.layout = QtWidgets.QGridLayout() top_widget = QtWidgets.QWidget() @@ -303,14 +341,69 @@ 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_colors() + palette = self.modify_palette(colors) + self.setPalette(palette) + self.style = CustomTitleSyle(self) + self.setStyle(self.style) + 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_colors(self): + palette = QtWidgets.QApplication.palette() + defaults = { + 'window': palette.color(QtGui.QPalette.ColorRole.Window), + 'title_bar': palette.color(QtGui.QPalette.ColorRole.Highlight) + } + colors = { + key: QtGui.QColor(self.manager.get_color(self.expurl, key) or default.name()) + for key, default in defaults.items() + } + colors['window_text'] = QtGui.QColor( + '#000000' if colors['window'].lightness() > 128 else '#FFFFFF') + return 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 +550,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 +577,21 @@ def set_explist_model(self, model): def set_schedule_model(self, model): self.schedule = model.backing_store + def get_color(self, expurl, key): + return self.colors.get(expurl, {}).get(key) + + 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): + return self.colors.get(expurl, {}) + + 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 +701,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 +818,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 +830,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)