From d485d079bdd958225482254e3f5c8c75a5b9fd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Sun, 2 Feb 2025 16:09:13 +0100 Subject: [PATCH 01/14] Add multiple MDI areas #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/dashboard/experiments.py | 32 +++++++++++++++------- artiq/frontend/artiq_dashboard.py | 44 +++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/artiq/dashboard/experiments.py b/artiq/dashboard/experiments.py index 10bfa5a0c2..f103da207c 100644 --- a/artiq/dashboard/experiments.py +++ b/artiq/dashboard/experiments.py @@ -576,30 +576,42 @@ def get_submission_arguments(self, expurl): def open_experiment(self, expurl): if expurl in self.open_experiments: dock = self.open_experiments[expurl] + mdi_area = dock.mdiArea() + if mdi_area is not None: + tab_widget = self.main_window.centralWidget() + tab_widget.setCurrentWidget(mdi_area) + mdi_area.setActiveSubWindow(dock) + if dock.isMinimized(): dock.showNormal() - self.main_window.centralWidget().setActiveSubWindow(dock) return dock try: dock = _ExperimentDock(self, expurl) - except: - logger.warning("Failed to create experiment dock for %s, " - "attempting to reset arguments", expurl, - exc_info=True) + except Exception: + logger.warning( + "Failed to create experiment dock for %s, attempting to reset arguments", + expurl, + exc_info=True, + ) del self.submission_arguments[expurl] dock = _ExperimentDock(self, expurl) self.open_experiments[expurl] = dock dock.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.main_window.centralWidget().addSubWindow(dock) + + mdi_area = self.main_window.centralWidget().currentWidget() + if mdi_area is not None: + mdi_area.addSubWindow(dock) dock.show() dock.sigClosed.connect(partial(self.on_dock_closed, expurl)) if expurl in self.dock_states: try: dock.restore_state(self.dock_states[expurl]) - except: - logger.warning("Failed to restore dock state when opening " - "experiment %s", expurl, - exc_info=True) + except Exception: + logger.warning( + "Failed to restore dock state when opening experiment %s", + expurl, + exc_info=True, + ) return dock def on_dock_closed(self, expurl): diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index a0606c3915..3a860cd749 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -74,6 +74,43 @@ def __init__(self, server): self.exit_request = asyncio.Event() + self.tab_widget = QtWidgets.QTabWidget() + self.tab_widget.setTabsClosable(True) + self.tab_widget.tabCloseRequested.connect(self.close_mdi_area) + self.setCentralWidget(self.tab_widget) + self.add_mdi_area("Main Area") + toolbar = QtWidgets.QToolBar("Main Toolbar") + toolbar.setObjectName("MainToolbar") + self.addToolBar(toolbar) + + add_area_action = QtWidgets.QAction("New Workspace", self) + add_area_action.triggered.connect(self.new_mdi_area) + toolbar.addAction(add_area_action) + + def add_mdi_area(self, title): + """Create a new MDI area (tab) with the given title.""" + mdi_area = MdiArea() + self.tab_widget.addTab(mdi_area, title) + + def new_mdi_area(self): + """Add a new MDI area (tab) with an auto-generated title.""" + count = self.tab_widget.count() + 1 + title = f"Workspace {count}" + self.add_mdi_area(title) + self.tab_widget.setCurrentIndex(self.tab_widget.count() - 1) + + def close_mdi_area(self, index): + """Handle closing an MDI area (tab).""" + if self.tab_widget.count() == 1: + logging.warning("Cannot close last workspace") + return + mdi_area = self.tab_widget.widget(index) + for experiment in mdi_area.subWindowList(): + mdi_area.removeSubWindow(experiment) + experiment.close() + self.tab_widget.removeTab(index) + mdi_area.deleteLater() + def closeEvent(self, event): event.ignore() self.exit_request.set() @@ -106,6 +143,8 @@ def __init__(self): QtGui.QKeySequence('Ctrl+Shift+C'), self) self.cascade.activated.connect( lambda: self.cascadeSubWindows()) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) def paintEvent(self, event): QtWidgets.QMdiArea.paintEvent(self, event) @@ -115,7 +154,6 @@ def paintEvent(self, event): painter.setOpacity(0.5) painter.drawPixmap(x, y, self.pixmap) - def main(): # initialize application args = get_argparser().parse_args() @@ -185,10 +223,6 @@ def report_disconnect(): # initialize main window main_window = MainWindow(args.server if server_name is None else server_name) smgr.register(main_window) - mdi_area = MdiArea() - mdi_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - mdi_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - main_window.setCentralWidget(mdi_area) # create UI components expmgr = experiments.ExperimentManager(main_window, From 04bc5302163975e531eb35d4eb69ac50825f5def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Sun, 2 Feb 2025 22:59:36 +0100 Subject: [PATCH 02/14] Add double-click action to change the name of the MDI area #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 3a860cd749..6ac63f48ab 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -61,6 +61,25 @@ def get_argparser(): return parser +class EditableTabBar(QtWidgets.QTabBar): + def mouseDoubleClickEvent(self, event): + index = self.tabAt(event.pos()) + if index != -1: + current_name = self.tabText(index) + new_name, ok = QtWidgets.QInputDialog.getText( + self, "Rename Workspace", "Enter a new name:", + text=current_name + ) + if ok and new_name.strip(): + new_name = new_name.strip() + self.setTabText(index, new_name) + tab_widget = self.parent() + if isinstance(tab_widget, QtWidgets.QTabWidget): + mdi_area = tab_widget.widget(index) + mdi_area.tab_name = new_name + super().mouseDoubleClickEvent(event) + + class MainWindow(QtWidgets.QMainWindow): def __init__(self, server): QtWidgets.QMainWindow.__init__(self) @@ -75,6 +94,7 @@ def __init__(self, server): self.exit_request = asyncio.Event() self.tab_widget = QtWidgets.QTabWidget() + self.tab_widget.setTabBar(EditableTabBar()) self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested.connect(self.close_mdi_area) self.setCentralWidget(self.tab_widget) From ac5c8c1823fec1f9fe1673c77d35b529a1a3e5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Mon, 3 Feb 2025 22:33:28 +0100 Subject: [PATCH 03/14] Restore the state of the MainWindow before restoring the state of the ExperimentManager #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 6ac63f48ab..6f05130e76 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -242,7 +242,6 @@ def report_disconnect(): # initialize main window main_window = MainWindow(args.server if server_name is None else server_name) - smgr.register(main_window) # create UI components expmgr = experiments.ExperimentManager(main_window, @@ -252,6 +251,7 @@ def report_disconnect(): rpc_clients["schedule"], rpc_clients["experiment_db"]) smgr.register(expmgr) + smgr.register(main_window) d_shortcuts = shortcuts.ShortcutsDock(main_window, expmgr) smgr.register(d_shortcuts) d_explorer = explorer.ExplorerDock(expmgr, d_shortcuts, From 90ede74a41927c06db9d60d1668106e7c74f310d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Mon, 3 Feb 2025 23:10:08 +0100 Subject: [PATCH 04/14] Add saving/restoration of mdi_areas #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/dashboard/experiments.py | 11 ++++++++++- artiq/frontend/artiq_dashboard.py | 23 +++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/artiq/dashboard/experiments.py b/artiq/dashboard/experiments.py index f103da207c..3797b6124e 100644 --- a/artiq/dashboard/experiments.py +++ b/artiq/dashboard/experiments.py @@ -713,13 +713,17 @@ async def open_file(self, file): def save_state(self): for expurl, dock in self.open_experiments.items(): self.dock_states[expurl] = dock.save_state() + experiment_mdi = {} + for expurl, dock in self.open_experiments.items(): + experiment_mdi[expurl] = dock.mdiArea().tab_name return { "scheduling": self.submission_scheduling, "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()) + "open_docks": set(self.open_experiments.keys()), + "experiment_mdi": experiment_mdi } def restore_state(self, state): @@ -730,7 +734,12 @@ def restore_state(self, state): self.submission_options = state["options"] self.submission_arguments = state["arguments"] self.argument_ui_names = state.get("argument_uis", {}) + experiment_mdi = state.get("experiment_mdi", {}) for expurl in state["open_docks"]: + tab_widget = self.main_window.centralWidget() + mdi_area_name = experiment_mdi[expurl] + mdi_area = self.main_window.get_mdi_area_by_name(mdi_area_name) + tab_widget.setCurrentWidget(mdi_area) self.open_experiment(expurl) def show_quick_open(self): diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 6f05130e76..25657cb0a6 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -98,7 +98,6 @@ def __init__(self, server): self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested.connect(self.close_mdi_area) self.setCentralWidget(self.tab_widget) - self.add_mdi_area("Main Area") toolbar = QtWidgets.QToolBar("Main Toolbar") toolbar.setObjectName("MainToolbar") self.addToolBar(toolbar) @@ -110,6 +109,7 @@ def __init__(self, server): def add_mdi_area(self, title): """Create a new MDI area (tab) with the given title.""" mdi_area = MdiArea() + mdi_area.tab_name = title # store the name for later lookup self.tab_widget.addTab(mdi_area, title) def new_mdi_area(self): @@ -136,14 +136,33 @@ def closeEvent(self, event): self.exit_request.set() def save_state(self): + """ + Save MainWindow state including MDI areas. + (This is separate from the QMainWindow state.) + """ + mdi_areas = [self.tab_widget.tabText(i) for i in range(self.tab_widget.count())] return { "state": bytes(self.saveState()), - "geometry": bytes(self.saveGeometry()) + "geometry": bytes(self.saveGeometry()), + "mdi_areas": mdi_areas, } def restore_state(self, state): + """Restore MainWindow state including MDI areas.""" self.restoreGeometry(QtCore.QByteArray(state["geometry"])) self.restoreState(QtCore.QByteArray(state["state"])) + for title in state.get("mdi_areas", []): + self.add_mdi_area(title) + + def get_mdi_area_by_name(self, name): + """ + Given a name (i.e. tab title), return the corresponding MDI area. + Returns None if not found. + """ + for i in range(self.tab_widget.count()): + if self.tab_widget.tabText(i) == name: + return self.tab_widget.widget(i) + return None class MdiArea(QtWidgets.QMdiArea): From 06a9ad3b4e30a9f4e4f6de0b6fdcc2541b1da1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Tue, 4 Feb 2025 22:52:12 +0100 Subject: [PATCH 05/14] Avoid tab-names duplication #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 37 ++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 25657cb0a6..b11b9eef5c 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -61,6 +61,16 @@ def get_argparser(): return parser +def tab_name_exists(tab_widget, name, ignore_index=None): + for i in range(tab_widget.count()): + if ignore_index is not None and i == ignore_index: + continue + widget = tab_widget.widget(i) + if hasattr(widget, "tab_name") and widget.tab_name == name: + return True + return False + + class EditableTabBar(QtWidgets.QTabBar): def mouseDoubleClickEvent(self, event): index = self.tabAt(event.pos()) @@ -72,14 +82,20 @@ def mouseDoubleClickEvent(self, event): ) if ok and new_name.strip(): new_name = new_name.strip() - self.setTabText(index, new_name) tab_widget = self.parent() + if isinstance(tab_widget, QtWidgets.QTabWidget): + if tab_name_exists(tab_widget, new_name, ignore_index=index): + QtWidgets.QMessageBox.warning( + self, "Duplicate Tab Name", + "Another workspace already has that name. Please choose a unique name." + ) + return + self.setTabText(index, new_name) if isinstance(tab_widget, QtWidgets.QTabWidget): mdi_area = tab_widget.widget(index) - mdi_area.tab_name = new_name + mdi_area.setTabName(new_name) super().mouseDoubleClickEvent(event) - class MainWindow(QtWidgets.QMainWindow): def __init__(self, server): QtWidgets.QMainWindow.__init__(self) @@ -107,10 +123,15 @@ def __init__(self, server): toolbar.addAction(add_area_action) def add_mdi_area(self, title): - """Create a new MDI area (tab) with the given title.""" + """Create a new MDI area (tab) with the given title, ensuring uniqueness.""" + unique_title = title + counter = 1 + while tab_name_exists(self.tab_widget, unique_title): + unique_title = f"{title} ({counter})" + counter += 1 mdi_area = MdiArea() - mdi_area.tab_name = title # store the name for later lookup - self.tab_widget.addTab(mdi_area, title) + mdi_area.setTabName(unique_title) + self.tab_widget.addTab(mdi_area, unique_title) def new_mdi_area(self): """Add a new MDI area (tab) with an auto-generated title.""" @@ -193,6 +214,10 @@ def paintEvent(self, event): painter.setOpacity(0.5) painter.drawPixmap(x, y, self.pixmap) + def setTabName(self, name): + self.tab_name = name + + def main(): # initialize application args = get_argparser().parse_args() From 67deba91d2917ed30a7a351bab3751cb4d320aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Fri, 7 Feb 2025 20:45:01 +0100 Subject: [PATCH 06/14] Handle the case of missing mda_area on fresh dashboard #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index b11b9eef5c..18d01031a0 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -121,6 +121,7 @@ def __init__(self, server): add_area_action = QtWidgets.QAction("New Workspace", self) add_area_action.triggered.connect(self.new_mdi_area) toolbar.addAction(add_area_action) + self.add_mdi_area("Workspace 1") def add_mdi_area(self, title): """Create a new MDI area (tab) with the given title, ensuring uniqueness.""" @@ -168,8 +169,31 @@ def save_state(self): "mdi_areas": mdi_areas, } + def _remove_init_mdi_areas(self): + """In order to handle the case of the first start of the + dashboard, we add new mdi_area in the init. It cannot be + done in restore_state because it is not called in that + special case. However, if the restore state is called, + we remove it. + """ + if self.tab_widget.count() == 1: + mdi_area = self.tab_widget.widget(0) + for experiment in mdi_area.subWindowList(): + mdi_area.removeSubWindow(experiment) + experiment.close() + self.tab_widget.removeTab(0) + mdi_area.deleteLater() + def restore_state(self, state): """Restore MainWindow state including MDI areas.""" + self._remove_init_mdi_areas() + if self.tab_widget.count() == 1: + mdi_area = self.tab_widget.widget(0) + for experiment in mdi_area.subWindowList(): + mdi_area.removeSubWindow(experiment) + experiment.close() + self.tab_widget.removeTab(0) + mdi_area.deleteLater() self.restoreGeometry(QtCore.QByteArray(state["geometry"])) self.restoreState(QtCore.QByteArray(state["state"])) for title in state.get("mdi_areas", []): From a6538d1dce2db28365b1f7b54f2831796ce0ba23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Fri, 7 Feb 2025 22:02:06 +0100 Subject: [PATCH 07/14] Replace New Workspace button with plus sign #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 18d01031a0..c85395569d 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -114,13 +114,13 @@ def __init__(self, server): self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested.connect(self.close_mdi_area) self.setCentralWidget(self.tab_widget) - toolbar = QtWidgets.QToolBar("Main Toolbar") - toolbar.setObjectName("MainToolbar") - self.addToolBar(toolbar) - add_area_action = QtWidgets.QAction("New Workspace", self) - add_area_action.triggered.connect(self.new_mdi_area) - toolbar.addAction(add_area_action) + plus_button = QtWidgets.QToolButton() + plus_button.setText("+") + plus_button.setToolTip("Add new workspace") + plus_button.clicked.connect(self.new_mdi_area) + self.tab_widget.setCornerWidget(plus_button, QtCore.Qt.TopLeftCorner) + self.add_mdi_area("Workspace 1") def add_mdi_area(self, title): From b6c80028e074c18fa2cfd32018341a40fb6bb82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Fri, 7 Feb 2025 22:07:45 +0100 Subject: [PATCH 08/14] Add tab tooltip - double click to rename #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index c85395569d..195218ea0c 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -132,7 +132,8 @@ def add_mdi_area(self, title): counter += 1 mdi_area = MdiArea() mdi_area.setTabName(unique_title) - self.tab_widget.addTab(mdi_area, unique_title) + index = self.tab_widget.addTab(mdi_area, unique_title) + self.tab_widget.setTabToolTip(index, "Double click to rename") def new_mdi_area(self): """Add a new MDI area (tab) with an auto-generated title.""" From 971beb326861081e73b54ded81fda5493f4e7c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Fri, 7 Feb 2025 22:23:41 +0100 Subject: [PATCH 09/14] Improve assignment of unique tab titles #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 195218ea0c..82e0dcb109 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -124,21 +124,19 @@ def __init__(self, server): self.add_mdi_area("Workspace 1") def add_mdi_area(self, title): - """Create a new MDI area (tab) with the given title, ensuring uniqueness.""" - unique_title = title - counter = 1 - while tab_name_exists(self.tab_widget, unique_title): - unique_title = f"{title} ({counter})" - counter += 1 + """Create a new MDI area (tab) with the given title.""" mdi_area = MdiArea() - mdi_area.setTabName(unique_title) - index = self.tab_widget.addTab(mdi_area, unique_title) + mdi_area.setTabName(title) + index = self.tab_widget.addTab(mdi_area, title) self.tab_widget.setTabToolTip(index, "Double click to rename") def new_mdi_area(self): - """Add a new MDI area (tab) with an auto-generated title.""" + """Add a new MDI area (tab) with an auto-generated unique title.""" count = self.tab_widget.count() + 1 title = f"Workspace {count}" + while tab_name_exists(self.tab_widget, title): + count = count + 1 + title = f"Workspace {count}" self.add_mdi_area(title) self.tab_widget.setCurrentIndex(self.tab_widget.count() - 1) From 68370e14bb199c6481ae7b123b9355e3404e7f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Tue, 11 Feb 2025 17:01:45 +0100 Subject: [PATCH 10/14] Force geomettry refresh on minimized windows on tab-change #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 82e0dcb109..67f083ec39 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -115,6 +115,8 @@ def __init__(self, server): self.tab_widget.tabCloseRequested.connect(self.close_mdi_area) self.setCentralWidget(self.tab_widget) + self.tab_widget.currentChanged.connect(self.on_tab_changed) + plus_button = QtWidgets.QToolButton() plus_button.setText("+") plus_button.setToolTip("Add new workspace") @@ -123,6 +125,14 @@ def __init__(self, server): self.add_mdi_area("Workspace 1") + def on_tab_changed(self, index): + mdi_area = self.tab_widget.widget(index) + if isinstance(mdi_area, MdiArea): + for subwindow in mdi_area.subWindowList(): + if subwindow.isMinimized(): + subwindow.setWindowState(QtCore.Qt.WindowNoState) + subwindow.setWindowState(QtCore.Qt.WindowMinimized) + def add_mdi_area(self, title): """Create a new MDI area (tab) with the given title.""" mdi_area = MdiArea() From 8398d660a18751baff7dd3ebb64c7b97694fa83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Thu, 6 Mar 2025 01:23:50 +0100 Subject: [PATCH 11/14] Keep focus on the last focused subwindow, instead of the minimized one #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 67f083ec39..cdb1be76b4 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -127,11 +127,15 @@ def __init__(self, server): def on_tab_changed(self, index): mdi_area = self.tab_widget.widget(index) + activeSubWindow = mdi_area.activeSubWindow() if isinstance(mdi_area, MdiArea): for subwindow in mdi_area.subWindowList(): if subwindow.isMinimized(): subwindow.setWindowState(QtCore.Qt.WindowNoState) subwindow.setWindowState(QtCore.Qt.WindowMinimized) + if activeSubWindow: + mdi_area.setActiveSubWindow(activeSubWindow) + activeSubWindow.widget().setFocus() def add_mdi_area(self, title): """Create a new MDI area (tab) with the given title.""" From 4ef9ba1823f84bde0c68a8a1be46f79d2acf16a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Thu, 6 Mar 2025 23:42:48 +0100 Subject: [PATCH 12/14] Keep the state of maximized windows, add comments #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index cdb1be76b4..43b8a80f63 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -126,16 +126,32 @@ def __init__(self, server): self.add_mdi_area("Workspace 1") def on_tab_changed(self, index): + """ + We want to refresh geometry to properly place minimized windows after resizing + from other MDI area. + It causes 2 other issues that are addressed here: + 1. The focus stays on the minimized window. + 2. If the code below executes, maximized windows get un-maximized - this is not + obvious and seems to depend on MDI implementation. + """ mdi_area = self.tab_widget.widget(index) + # Check which subwindow is active activeSubWindow = mdi_area.activeSubWindow() + # Check if active subwindow is maximized. If not, neither window is maximized + wasMaximized = activeSubWindow.isMaximized() if activeSubWindow else False + if isinstance(mdi_area, MdiArea): for subwindow in mdi_area.subWindowList(): + # Refresh geometry to properly place minimized windows if subwindow.isMinimized(): subwindow.setWindowState(QtCore.Qt.WindowNoState) subwindow.setWindowState(QtCore.Qt.WindowMinimized) + # Restore focus and maximization if activeSubWindow: mdi_area.setActiveSubWindow(activeSubWindow) activeSubWindow.widget().setFocus() + if wasMaximized: + activeSubWindow.setWindowState(QtCore.Qt.WindowMaximized) def add_mdi_area(self, title): """Create a new MDI area (tab) with the given title.""" From 758844b0f7fa7f52c9b76e4d0239f80bc86f4ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Sun, 9 Mar 2025 01:20:08 +0100 Subject: [PATCH 13/14] Support migration from release-8 to the version with multiple panes #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/dashboard/experiments.py | 16 ++++++++++++---- artiq/frontend/artiq_dashboard.py | 6 ++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/artiq/dashboard/experiments.py b/artiq/dashboard/experiments.py index 3797b6124e..1746dcaf8c 100644 --- a/artiq/dashboard/experiments.py +++ b/artiq/dashboard/experiments.py @@ -735,12 +735,20 @@ def restore_state(self, state): self.submission_arguments = state["arguments"] self.argument_ui_names = state.get("argument_uis", {}) experiment_mdi = state.get("experiment_mdi", {}) - for expurl in state["open_docks"]: + if 'experiment_mdi' in state: + for expurl in state["open_docks"]: + tab_widget = self.main_window.centralWidget() + mdi_area_name = experiment_mdi[expurl] + mdi_area = self.main_window.get_mdi_area_by_name(mdi_area_name) + tab_widget.setCurrentWidget(mdi_area) + self.open_experiment(expurl) + else: + # else statement is necessary for migration purpose tab_widget = self.main_window.centralWidget() - mdi_area_name = experiment_mdi[expurl] - mdi_area = self.main_window.get_mdi_area_by_name(mdi_area_name) + mdi_area = self.main_window.new_mdi_area() tab_widget.setCurrentWidget(mdi_area) - self.open_experiment(expurl) + for expurl in state["open_docks"]: + self.open_experiment(expurl) def show_quick_open(self): if self.is_quick_open_shown: diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 43b8a80f63..efa6cc6555 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -115,8 +115,6 @@ def __init__(self, server): self.tab_widget.tabCloseRequested.connect(self.close_mdi_area) self.setCentralWidget(self.tab_widget) - self.tab_widget.currentChanged.connect(self.on_tab_changed) - plus_button = QtWidgets.QToolButton() plus_button.setText("+") plus_button.setToolTip("Add new workspace") @@ -125,6 +123,8 @@ def __init__(self, server): self.add_mdi_area("Workspace 1") + self.tab_widget.currentChanged.connect(self.on_tab_changed) + def on_tab_changed(self, index): """ We want to refresh geometry to properly place minimized windows after resizing @@ -135,6 +135,8 @@ def on_tab_changed(self, index): obvious and seems to depend on MDI implementation. """ mdi_area = self.tab_widget.widget(index) + if not mdi_area: + return # Check which subwindow is active activeSubWindow = mdi_area.activeSubWindow() # Check if active subwindow is maximized. If not, neither window is maximized From 5987a5e13eb3a81a455b3b0050862494ece8f443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Malczak?= Date: Tue, 15 Apr 2025 20:51:21 +0200 Subject: [PATCH 14/14] Linter changes #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miłosz Malczak --- artiq/frontend/artiq_dashboard.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index efa6cc6555..993f8bc78d 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -84,10 +84,12 @@ def mouseDoubleClickEvent(self, event): new_name = new_name.strip() tab_widget = self.parent() if isinstance(tab_widget, QtWidgets.QTabWidget): - if tab_name_exists(tab_widget, new_name, ignore_index=index): + if tab_name_exists(tab_widget, new_name, + ignore_index=index): QtWidgets.QMessageBox.warning( self, "Duplicate Tab Name", - "Another workspace already has that name. Please choose a unique name." + "Another workspace already has that name. " + "Please choose a unique name." ) return self.setTabText(index, new_name) @@ -96,6 +98,7 @@ def mouseDoubleClickEvent(self, event): mdi_area.setTabName(new_name) super().mouseDoubleClickEvent(event) + class MainWindow(QtWidgets.QMainWindow): def __init__(self, server): QtWidgets.QMainWindow.__init__(self) @@ -127,19 +130,20 @@ def __init__(self, server): def on_tab_changed(self, index): """ - We want to refresh geometry to properly place minimized windows after resizing - from other MDI area. + We want to refresh geometry to properly place minimized windows after + resizing from other MDI area. It causes 2 other issues that are addressed here: 1. The focus stays on the minimized window. - 2. If the code below executes, maximized windows get un-maximized - this is not - obvious and seems to depend on MDI implementation. + 2. If the code below executes, maximized windows get un-maximized - + this is not obvious and seems to depend on MDI implementation. """ mdi_area = self.tab_widget.widget(index) if not mdi_area: return # Check which subwindow is active activeSubWindow = mdi_area.activeSubWindow() - # Check if active subwindow is maximized. If not, neither window is maximized + # Check if active subwindow is maximized. If not, neither window is + # maximized wasMaximized = activeSubWindow.isMaximized() if activeSubWindow else False if isinstance(mdi_area, MdiArea):