From 12da1bc617d9d0559d6440a77608db44bf1498dc Mon Sep 17 00:00:00 2001 From: Maciej Bartkowiak Date: Fri, 3 Jul 2026 13:28:49 +0100 Subject: [PATCH 1/4] Move plot creator buttons to one side --- .../Tabs/Visualisers/DataPlotter.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py index c714257e4e..77a5a4156d 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py @@ -18,6 +18,7 @@ from qtpy.QtCore import Signal, Slot from qtpy.QtWidgets import ( QHBoxLayout, + QLabel, QMessageBox, QPushButton, QTableView, @@ -47,30 +48,48 @@ class DataPlotter(QWidget): def __init__(self, *args, unit_lookup=None, **kwargs): super().__init__(*args, **kwargs) - + self.tab_index, self.tab_count = 1, 1 self._unit_lookup = unit_lookup layout = QVBoxLayout(self) - button_bar = QWidget(self) - button_layout = QHBoxLayout(button_bar) + control_bar = QWidget(self) + bar_layout = QHBoxLayout(control_bar) self._selection_viewer = QTableView(self) layout.addWidget(self._selection_viewer) - layout.addWidget(button_bar) + layout.addWidget(control_bar) + button_bar = self.create_buttons() + bar_layout.addWidget(button_bar) + plotter_preview = self.create_preview() + bar_layout.addWidget(plotter_preview) + self._model = PlottingContext( + unit_lookup=self._unit_lookup, + ) + self._selection_viewer.setModel(self._model) + self.hide_columns() + + def create_buttons(self) -> QWidget: + button_bar = QWidget(self) + button_layout = QVBoxLayout(button_bar) buttons = [ - ("Plot Data", self.plot_data), - ("Clear", self.clear), - ("New Plot", self.new_plot), - ("New Data View (Text)", self.new_text), + ("New empty plot", self.new_plot), + ("New empty text view", self.new_text), + ("Send data to plotter", self.plot_data), + ("Clear data selection", self.clear), ] for name, function in buttons: button = QPushButton(name, button_bar) button_layout.addWidget(button) if function is not None: button.clicked.connect(function) - self._model = PlottingContext( - unit_lookup=self._unit_lookup, + return button_bar + + def create_preview(self) -> QWidget: + previewer = QWidget(self) + previewer_layout = QVBoxLayout(previewer) + self.target_label = QLabel( + f"Datasets will be sent to plotter tab {self.tab_index} out of {self.tab_count}" ) - self._selection_viewer.setModel(self._model) - self.hide_columns() + self.target_label.setWordWrap(True) + previewer_layout.addWidget(self.target_label) @Slot(object) def add_dataset(self, dataset: SingleDataset): From b9876dfe6cbb8379985c54878802a586d713ee90 Mon Sep 17 00:00:00 2001 From: Maciej Bartkowiak Date: Fri, 3 Jul 2026 17:17:28 +0100 Subject: [PATCH 2/4] Pass information from PlotHolder back to PlotCreator --- MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py | 12 ++++++ MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py | 2 + .../Tabs/Visualisers/DataPlotter.py | 37 +++++++++++++++++-- .../MDANSE_GUI/Tabs/Visualisers/PlotHolder.py | 19 +++++++++- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py b/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py index eb624f3eff..b322681e5a 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py @@ -161,6 +161,18 @@ def __init__( self._tabs["Instruments"]._visualiser.instrument_details_changed.connect( self._tabs["Actions"].update_action_after_instrument_change ) + self._tabs["Plot Holder"]._visualiser.current_tab_index.connect( + self._tabs["Plot Creator"]._visualiser.new_target_plot_index + ) + self._tabs["Plot Holder"]._visualiser.current_tab_count.connect( + self._tabs["Plot Creator"]._visualiser.new_target_plot_count + ) + self._tabs["Plot Holder"]._visualiser.datasets_in_plot.connect( + self._tabs["Plot Creator"]._visualiser.new_dataset_count_in_target + ) + self._tabs["Plot Holder"]._visualiser.plot_widget_type.connect( + self._tabs["Plot Creator"]._visualiser.new_plot_widget_type + ) # connect signal to the tab self.signal_recent_trajectory_file.connect( self._tabs["Trajectories"].load_trajectory diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py index 2e1346e2e6..9df918e0b0 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py @@ -127,7 +127,9 @@ def accept_external_data(self, data_model): LOG.error(f"Visualiser failed to plot data: {e2}") else: self.tab_notification() + self._visualiser.send_plot_info() @Slot(int) def switch_model(self, tab_id): self._view.setModel(self.model) + self._visualiser.send_plot_info() diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py index 77a5a4156d..6584fcf054 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py @@ -48,7 +48,9 @@ class DataPlotter(QWidget): def __init__(self, *args, unit_lookup=None, **kwargs): super().__init__(*args, **kwargs) - self.tab_index, self.tab_count = 1, 1 + self.tab_index, self.tab_count = 0, 1 + self.dataset_count = 0 + self.plotter_type = "PlotWidget" self._unit_lookup = unit_lookup layout = QVBoxLayout(self) control_bar = QWidget(self) @@ -85,11 +87,38 @@ def create_buttons(self) -> QWidget: def create_preview(self) -> QWidget: previewer = QWidget(self) previewer_layout = QVBoxLayout(previewer) - self.target_label = QLabel( - f"Datasets will be sent to plotter tab {self.tab_index} out of {self.tab_count}" - ) + self.target_label = QLabel("Target plot in next tab.") self.target_label.setWordWrap(True) previewer_layout.addWidget(self.target_label) + self.update_target_plot_label() + return previewer + + def update_target_plot_label(self): + self.target_label.setText( + "Datasets will be sent to the Plot Holder tab.
" + f"They will appear in tab {self.tab_index + 1} out of {self.tab_count}.
" + f"It is a {self.plotter_type}, currently containing {self.dataset_count} datasets." + ) + + @Slot(int) + def new_target_plot_index(self, new_index: int): + self.tab_index = new_index + self.update_target_plot_label() + + @Slot(int) + def new_target_plot_count(self, new_count: int): + self.tab_count = new_count + self.update_target_plot_label() + + @Slot(int) + def new_dataset_count_in_target(self, new_dataset_count: int): + self.dataset_count = new_dataset_count + self.update_target_plot_label() + + @Slot(str) + def new_plot_widget_type(self, new_plot_widget_type: str): + self.plotter_type = new_plot_widget_type + self.update_target_plot_label() @Slot(object) def add_dataset(self, dataset: SingleDataset): diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py index 1a1f1360b2..0a9ea91652 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py @@ -34,6 +34,10 @@ class PlotHolder(QTabWidget): error = Signal(str) new_entry = Signal() + current_tab_index = Signal(int) + current_tab_count = Signal(int) + datasets_in_plot = Signal(int) + plot_widget_type = Signal(str) def __init__(self, *args, unit_lookup=None, **kwargs): super().__init__(*args, **kwargs) @@ -75,6 +79,7 @@ def new_plot(self, tab_name: str) -> int: self._plotter.append(plotter) self.setCurrentIndex(tab_id) self.new_entry.emit() + self.send_plot_info() return tab_id @Slot(str) @@ -91,6 +96,7 @@ def new_text(self, ignored_name: str) -> int: self._plotter.append(plotter) self.setCurrentIndex(tab_id) self.new_entry.emit() + self.send_plot_info() return tab_id @Slot(int) @@ -108,9 +114,10 @@ def clean_up_closed_tab(self, tab_id: int): self._current_id = valid_id_values[0] self.setCurrentIndex(self._current_id) self.removeTab(tab_id) + self.send_plot_info() @property - def model(self): + def model(self) -> PlottingContext: tab_id = self.currentIndex() try: pc = self._context[tab_id] @@ -122,7 +129,7 @@ def model(self): return pc @property - def plotter(self): + def plotter(self) -> DataWidget: tab_id = self.currentIndex() try: return self._plotter[tab_id] @@ -164,3 +171,11 @@ def update_plot(self, plot_number: int): except Exception: LOG.error("Plotting failed: %s", traceback.format_exc()) plotter.plot_blank() + else: + self.send_plot_info() + + def send_plot_info(self): + self.current_tab_count.emit(len(self._plotter)) + self.current_tab_index.emit(self.currentIndex()) + self.datasets_in_plot.emit(self.model.rowCount()) + self.plot_widget_type.emit(type(self.plotter).__name__) From 105394f3fb3f1945134d25febc2843864f94db33 Mon Sep 17 00:00:00 2001 From: Maciej Bartkowiak Date: Fri, 3 Jul 2026 17:54:02 +0100 Subject: [PATCH 3/4] Show preview of current plot contents in plot creator --- MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py | 3 ++ MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py | 11 ++++- .../Tabs/Visualisers/DataPlotter.py | 42 +++++++++++++------ 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py b/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py index b322681e5a..1b11e8b9f9 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py @@ -173,6 +173,9 @@ def __init__( self._tabs["Plot Holder"]._visualiser.plot_widget_type.connect( self._tabs["Plot Creator"]._visualiser.new_plot_widget_type ) + self._tabs["Plot Holder"].connect_external_view( + self._tabs["Plot Creator"]._visualiser.preview_table + ) # connect signal to the tab self.signal_recent_trajectory_file.connect( self._tabs["Trajectories"].load_trajectory diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py index 9df918e0b0..605d0e6c3e 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotTab.py @@ -29,7 +29,7 @@ from MDANSE_GUI.Widgets.PlotSettingsDialog import PlotSettingsEditor if TYPE_CHECKING: - from qtpy.QtWidgets import QDialog, QWidget + from qtpy.QtWidgets import QAbstractItemView, QDialog, QWidget from MDANSE_GUI.Session.Session import Session @@ -66,6 +66,12 @@ def __init__(self, *args, **kwargs): ) self.matplotlib_dialog.values_changed.connect(self._visualiser.update_plots) self._core.add_button("Change matplotlib settings", self.edit_matplotlib) + self.connected_views = [self._view] + + def connect_external_view(self, new_view: QAbstractItemView): + if new_view not in self.connected_views: + self.connected_views.append(new_view) + new_view.setModel(self.model) def launch_dialog(self, dialog: QDialog) -> None: if dialog.isVisible(): @@ -131,5 +137,6 @@ def accept_external_data(self, data_model): @Slot(int) def switch_model(self, tab_id): - self._view.setModel(self.model) + for view in self.connected_views: + view.setModel(self.model) self._visualiser.send_plot_info() diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py index 6584fcf054..1e80d8aeea 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py @@ -17,6 +17,7 @@ from qtpy.QtCore import Signal, Slot from qtpy.QtWidgets import ( + QGroupBox, QHBoxLayout, QLabel, QMessageBox, @@ -71,17 +72,25 @@ def __init__(self, *args, unit_lookup=None, **kwargs): def create_buttons(self) -> QWidget: button_bar = QWidget(self) button_layout = QVBoxLayout(button_bar) - buttons = [ - ("New empty plot", self.new_plot), - ("New empty text view", self.new_text), - ("Send data to plotter", self.plot_data), - ("Clear data selection", self.clear), - ] - for name, function in buttons: - button = QPushButton(name, button_bar) - button_layout.addWidget(button) - if function is not None: - button.clicked.connect(function) + button_groups = { + "Empty plot creation": [ + ("New empty plot", self.new_plot), + ("New empty text view", self.new_text), + ], + "Current data selection": [ + ("Send data to plotter", self.plot_data), + ("Clear data selection", self.clear), + ], + } + for group_name, buttons in button_groups.items(): + subgroup = QGroupBox(group_name, button_bar) + sublayout = QVBoxLayout(subgroup) + for name, function in buttons: + button = QPushButton(name, button_bar) + sublayout.addWidget(button) + if function is not None: + button.clicked.connect(function) + button_layout.addWidget(subgroup) return button_bar def create_preview(self) -> QWidget: @@ -90,15 +99,24 @@ def create_preview(self) -> QWidget: self.target_label = QLabel("Target plot in next tab.") self.target_label.setWordWrap(True) previewer_layout.addWidget(self.target_label) + info_label = QLabel( + f"Contents of the currently selected {self.plotter_type} in the next tab:" + ) + previewer_layout.addWidget(info_label) + self.preview_table = QTableView(previewer) + previewer_layout.addWidget(self.preview_table) self.update_target_plot_label() return previewer def update_target_plot_label(self): self.target_label.setText( - "Datasets will be sent to the Plot Holder tab.
" + "Datasets listed above will be sent to the Plot Holder tab.
" f"They will appear in tab {self.tab_index + 1} out of {self.tab_count}.
" f"It is a {self.plotter_type}, currently containing {self.dataset_count} datasets." ) + for col_num in range(4, 10): + self.preview_table.hideColumn(col_num) + self.preview_table.resizeColumnsToContents() @Slot(int) def new_target_plot_index(self, new_index: int): From 9f4722380260f7b0eee166c73f2a99244d208d55 Mon Sep 17 00:00:00 2001 From: Maciej Bartkowiak Date: Mon, 6 Jul 2026 11:43:03 +0100 Subject: [PATCH 4/4] Differentiate between plotter and text viewer in GUI text --- .../MDANSE_GUI/Tabs/Visualisers/DataPlotter.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py index 1e80d8aeea..af49f9cfca 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataPlotter.py @@ -33,6 +33,8 @@ SingleDataset, ) +WIDGET_DESCRIPTIONS = {"DataWidget": "text viewer", "PlotWidget": "plotter"} + class DataPlotter(QWidget): """Part of PlotCreator which sends datasets to the plotter. @@ -70,6 +72,7 @@ def __init__(self, *args, unit_lookup=None, **kwargs): self.hide_columns() def create_buttons(self) -> QWidget: + self._plotting_button_reference = None button_bar = QWidget(self) button_layout = QVBoxLayout(button_bar) button_groups = { @@ -90,6 +93,8 @@ def create_buttons(self) -> QWidget: sublayout.addWidget(button) if function is not None: button.clicked.connect(function) + if name == "Send data to plotter": + self._plotting_button_reference = button button_layout.addWidget(subgroup) return button_bar @@ -99,21 +104,27 @@ def create_preview(self) -> QWidget: self.target_label = QLabel("Target plot in next tab.") self.target_label.setWordWrap(True) previewer_layout.addWidget(self.target_label) - info_label = QLabel( + self.info_label = QLabel( f"Contents of the currently selected {self.plotter_type} in the next tab:" ) - previewer_layout.addWidget(info_label) + previewer_layout.addWidget(self.info_label) self.preview_table = QTableView(previewer) previewer_layout.addWidget(self.preview_table) self.update_target_plot_label() return previewer def update_target_plot_label(self): + output_widget = WIDGET_DESCRIPTIONS.get(self.plotter_type, "data visualiser") + self.info_label.setText( + f"Contents of the currently selected {output_widget} in the next tab:" + ) self.target_label.setText( "Datasets listed above will be sent to the Plot Holder tab.
" f"They will appear in tab {self.tab_index + 1} out of {self.tab_count}.
" - f"It is a {self.plotter_type}, currently containing {self.dataset_count} datasets." + f"It is a {output_widget}, " + f"currently containing {self.dataset_count} datasets." ) + self._plotting_button_reference.setText(f"Send data to {output_widget}") for col_num in range(4, 10): self.preview_table.hideColumn(col_num) self.preview_table.resizeColumnsToContents()