← Back to MainWindow documentation
MainWindow - Source Code¶
File: Distributed_Design_Optimizer/postprocess/widgets/MainWindow.py
# Copyright (C) The DistributedDesignOptimizer Contributors
# Licensed under the GNU General Public License v3.0. See LICENSE file for details.
#
# Additional Request:
# We kindly request that any modifications or changes to the source code be
# shared with us by submitting them as pull requests to our GitHub repository.
# This request is not legally binding but is made in the spirit of
# collaboration and open-source development.
"""Main application window for the DDO Viewer."""
import logging
from PySide6.QtWidgets import (
QMainWindow, QDockWidget, QTabWidget, QTabBar,
QStatusBar, QMessageBox, QWidget, QSizePolicy, QApplication,
QLabel, QProgressBar, QStyle, QHBoxLayout, QVBoxLayout,
QPushButton, QStackedWidget, QFrame, QComboBox,
)
from PySide6.QtCore import Qt, QSettings, QTimer
from PySide6.QtGui import QAction, QCloseEvent
from ..styles.Theme import get_accent_color, PRIMARY, PRIMARY_LIGHT, SECONDARY, PRIMARY_DARK
from ..handlers.DataHandler import DataHandler
from ..handlers.PlotHandler import PlotHandler
from ..handlers.TopologyHandler import TopologyHandler
from ..handlers.ChatHandler import ChatHandler
from .DataSelectionPanel import DataSelectionPanel
from .PlotCanvas import PlotCanvas
from .TopologyPanel import TopologyPanel
from .PlotSettingsPanel import PlotSettingsPanel
from .ChatPanel import ChatPanel
from .TopologyControlsWidget import TopologyControlsWidget
from ._auto_update_delegate import AutoUpdateDelegate
from ._topology_delegate import TopologyDelegate
logger = logging.getLogger(__name__)
# Workflow stage indices
_TAB_DATA = 0
_TAB_VIS = 1
_TAB_SETTINGS = 2
_TAB_CHAT = 3
_STEP_COUNT = 4
# Activity-bar button stylesheet templates
_ACTIVITY_BTN_STYLE = (
"QPushButton {{"
" background: transparent; border: none; color: #666;"
" font-size: 9pt; padding: 10px 4px;"
" border-left: 3px solid transparent;"
"}}"
"QPushButton:checked {{"
" background-color: #f0f0f0; color: {primary};"
" font-weight: bold; border-left: 3px solid {primary};"
"}}"
"QPushButton:disabled {{"
" color: #c0c0c0;"
"}}"
"QPushButton:hover:!disabled:!checked {{"
" background-color: #e8e8e8;"
"}}"
).format(primary=PRIMARY)
class MainWindow(QMainWindow):
"""Top-level application window.
Layout:
Left dock — action bar + QTabWidget with Data / Settings / AI Chat tabs
Central — QTabWidget with Plot / Topology (with embedded controls)
"""
def __init__(self, llm_config: dict | None = None) -> None:
"""Initialize the main window and all child widgets.
Args:
llm_config: Optional LLM configuration dict for the AI chat handler.
"""
super().__init__()
self.setWindowTitle("DDO Viewer — Distributed Optimization Processing")
self.setMinimumSize(1000, 700)
self.resize(1400, 900)
self._settings = QSettings("DDO", "DDOViewer")
# --- Create handlers ---
self._data_handler = DataHandler(self)
self._plot_handler = PlotHandler(self)
self._topology_handler = TopologyHandler(self)
self._chat_handler = ChatHandler(llm_config or {}, self)
# --- Delegates (initialized after layout setup) ---
self._auto_update_delegate: AutoUpdateDelegate | None = None
self._topology_delegate: TopologyDelegate | None = None
# --- Create widgets ---
self._data_panel = DataSelectionPanel()
self._data_panel.set_data_handler(self._data_handler)
self._plot_canvas = PlotCanvas()
self._topology_panel = TopologyPanel()
self._settings_panel = PlotSettingsPanel()
self._chat_panel = ChatPanel()
# Track workflow state
self._vis_type: str | None = None # "lineplot" or "topology"
self._has_visualization = False # True once a plot/topology is generated
# --- Settings stack: placeholder / plot settings / topology controls ---
self._settings_placeholder = QWidget()
ph_layout = QVBoxLayout(self._settings_placeholder)
ph_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
ph_icon = QLabel()
ph_icon.setPixmap(
self.style()
.standardIcon(QStyle.StandardPixmap.SP_FileDialogDetailedView)
.pixmap(48, 48)
)
ph_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
ph_layout.addWidget(ph_icon)
ph_title = QLabel("Settings")
ph_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
ph_title.setStyleSheet("font-size: 13pt; font-weight: bold;")
ph_layout.addWidget(ph_title)
ph_sub = QLabel(
"Select a visualization type first,\n"
"then configure its settings here."
)
ph_sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
ph_sub.setStyleSheet("color: gray; font-size: 10pt;")
ph_layout.addWidget(ph_sub)
self._settings_stack = QStackedWidget()
self._settings_stack.addWidget(self._settings_placeholder) # page 0: placeholder
self._settings_stack.addWidget(self._settings_panel) # page 1: plot settings
self._topology_controls = TopologyControlsWidget()
self._topology_controls.launch_requested.connect(
self._topology_panel.topology_launch_requested.emit
)
self._settings_stack.addWidget(self._topology_controls) # page 2: topology controls
self._settings_stack.setCurrentIndex(0)
# --- Visualization chooser widget ---
self._vis_chooser = self._create_vis_chooser()
# --- Assemble layout ---
self._setup_central_widget()
self._setup_left_dock()
self._setup_status_bar()
# --- Wire signals ---
self._connect_signals()
# --- Restore layout state ---
self._restore_state()
# ==================================================================
# Layout Setup
# ==================================================================
def _create_vis_chooser(self) -> QWidget:
"""Build the Visualization chooser panel with LinePlot / Topology segmented control."""
w = QWidget()
layout = QVBoxLayout(w)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(12)
title = QLabel("Choose Visualization Type")
title.setStyleSheet("font-size: 13pt; font-weight: bold;")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
desc = QLabel("Select how you want to visualize the loaded data.")
desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc.setStyleSheet("color: gray; font-size: 10pt;")
desc.setWordWrap(True)
layout.addWidget(desc)
layout.addSpacing(8)
# Dropdown: Line Plot / Topology
self._vis_segment = QComboBox()
self._vis_segment.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
self._vis_segment.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
self._vis_segment.addItems(["Line Plot", "Topology"])
self._vis_segment.activated.connect(self._on_vis_segment_changed)
layout.addWidget(self._vis_segment)
layout.addSpacing(16)
self._clear_graph_btn = QPushButton("Clear Graph")
self._clear_graph_btn.setMinimumHeight(36)
self._clear_graph_btn.setStyleSheet(
f"QPushButton {{ background-color: #cc4444; color: white; "
f"font-weight: bold; padding: 5px 12px; border-radius: 3px; }}"
f"QPushButton:hover {{ background-color: #aa3333; }}"
f"QPushButton:disabled {{ background-color: #cccccc; color: #888888; }}"
)
self._clear_graph_btn.setToolTip("Clear the current visualization")
self._clear_graph_btn.setEnabled(False)
layout.addWidget(self._clear_graph_btn)
layout.addStretch()
return w
def _on_vis_segment_changed(self, index: int | None) -> None:
"""Handle segmented control selection."""
if index is None:
return
if index == 0:
self._on_vis_chosen("lineplot")
elif index == 1:
self._on_vis_chosen("topology")
def _setup_central_widget(self) -> None:
"""Central area: stacked widget with Line Plot and Topology views."""
self._central_stack = QStackedWidget()
self._central_stack.addWidget(self._plot_canvas) # page 0: line plot
self._central_stack.addWidget(self._topology_panel) # page 1: topology
self._central_stack.setCurrentIndex(0)
self.setCentralWidget(self._central_stack)
def _setup_left_dock(self) -> None:
"""Left dock with VS Code-style vertical activity bar + stacked panels."""
# Create actions (used by auto-update delegate)
self._auto_update_action = QAction("Auto-Update", self)
self._auto_update_action.setCheckable(True)
self._auto_update_action.setToolTip("Enable automatic file-change detection")
self._refresh_action = QAction("Refresh", self)
self._refresh_action.setToolTip("Check for file changes and replot")
# Dock content: horizontal layout = activity bar | panel stack
dock_content = QWidget()
dock_h_layout = QHBoxLayout(dock_content)
dock_h_layout.setContentsMargins(0, 0, 0, 0)
dock_h_layout.setSpacing(0)
# --- Activity bar (narrow vertical column) ---
activity_bar = QWidget()
activity_bar.setStyleSheet(
"background-color: #f5f5f5; border-right: 1px solid #ddd;"
)
ab_layout = QVBoxLayout(activity_bar)
ab_layout.setContentsMargins(0, 4, 0, 4)
ab_layout.setSpacing(0)
# Build activity buttons: emoji + text in a single QPushButton
self._activity_btns: list[QPushButton] = []
btn_defs = [
("\U0001F4C2\nData", "Load and manage .dill history files"),
("\U0001F4CA\nVisualization", "Choose visualization type"),
("\U0001F527\nSettings", "Configure plot settings"),
("\U0001F916\nAI Chat", "Ask questions about the visualization"),
]
for i, (text, tooltip) in enumerate(btn_defs):
btn = QPushButton(text)
btn.setCheckable(True)
btn.setToolTip(tooltip)
btn.setStyleSheet(
"QPushButton {"
" background: transparent; border: none;"
" border-left: 3px solid transparent;"
" padding: 10px 6px; color: #555;"
"}"
"QPushButton:checked {"
f" background-color: white; border-left: 3px solid {PRIMARY};"
f" color: {PRIMARY}; font-weight: bold;"
"}"
"QPushButton:hover:!disabled:!checked {"
" background-color: #e8e8e8;"
"}"
"QPushButton:disabled {"
" color: #ccc;"
"}"
)
btn.clicked.connect(lambda checked, idx=i: self._on_activity_btn_clicked(idx))
ab_layout.addWidget(btn)
self._activity_btns.append(btn)
# Down-arrow connectors between buttons
for i in range(len(self._activity_btns) - 1):
arrow = QLabel("▾")
arrow.setAlignment(Qt.AlignmentFlag.AlignCenter)
arrow.setStyleSheet(f"color: {PRIMARY_LIGHT}; font-size: 16pt;")
# Insert arrow after button i — layout index = button_index + arrows_before + 1
ab_layout.insertWidget(ab_layout.indexOf(self._activity_btns[i]) + 1, arrow)
ab_layout.addStretch()
# Compute fixed width: measure widest label in bold + padding
from PySide6.QtGui import QFontMetrics, QFont as _QFont
bold_font = self.font()
bold_font.setBold(True)
fm_bold = QFontMetrics(bold_font)
max_text_w = 0
for text, _tooltip in btn_defs:
for line in text.split("\n"):
max_text_w = max(max_text_w, fm_bold.horizontalAdvance(line))
activity_bar.setFixedWidth(max_text_w + 30) # padding + border
dock_h_layout.addWidget(activity_bar)
# --- Panel stack (content area) ---
self._panel_stack = QStackedWidget()
self._panel_stack.addWidget(self._data_panel) # page 0
self._panel_stack.addWidget(self._vis_chooser) # page 1
self._panel_stack.addWidget(self._settings_stack) # page 2
self._panel_stack.addWidget(self._chat_panel) # page 3
self._panel_stack.setCurrentIndex(0)
dock_h_layout.addWidget(self._panel_stack, stretch=1)
# Select first button
self._activity_btns[0].setChecked(True)
# Initially disable panels 1-3
self._set_tab_enabled(_TAB_VIS, False)
self._set_tab_enabled(_TAB_SETTINGS, False)
self._set_tab_enabled(_TAB_CHAT, False)
# A1: Dock with hidden title bar
self._left_dock = QDockWidget(self)
self._left_dock.setObjectName("SidePanel")
self._left_dock.setWindowTitle("Side Panel")
self._left_dock.setTitleBarWidget(QWidget())
self._left_dock.setWidget(dock_content)
self._left_dock.setAllowedAreas(
Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea
)
self._left_dock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self._left_dock)
def _setup_menus(self) -> None:
"""No menu bar — all actions are accessible via buttons."""
# Menu bar intentionally removed per user request.
pass
def _setup_status_bar(self) -> None:
"""Status bar: left-aligned info widgets, right-aligned progress bar."""
self._status_bar = QStatusBar()
self.setStatusBar(self._status_bar)
# F2: Left-aligned file count and plot mode (addWidget, not addPermanentWidget)
self._file_count_label = QLabel("0 files")
self._file_count_label.setToolTip("Number of loaded history files")
self._status_bar.addWidget(self._file_count_label)
self._plot_mode_label = QLabel("")
self._plot_mode_label.setToolTip("Current Y-axis and X-axis modes")
self._status_bar.addWidget(self._plot_mode_label)
# F3: DPI-scaled progress bar (right-aligned)
fm = self.fontMetrics()
self._progress_bar = QProgressBar()
self._progress_bar.setFixedWidth(fm.averageCharWidth() * 20)
self._progress_bar.setMaximumHeight(fm.height() + 2)
self._progress_bar.setVisible(False)
self._status_bar.addPermanentWidget(self._progress_bar)
self._status_bar.showMessage("Ready")
# ==================================================================
# Signal Wiring
# ==================================================================
def _connect_signals(self) -> None:
"""Wire all cross-component signals."""
# Data panel → DataHandler
self._data_panel.files_load_requested.connect(self._data_handler.load_files)
# P19: auto-plot when selection changes
self._data_panel.selection_changed.connect(self._on_selection_changed)
# Clear All Data button in data panel
self._data_panel._clear_all_btn.clicked.connect(self._clear_data)
# DataHandler → Data panel
self._data_handler.entry_loaded.connect(self._data_panel.populate_tree)
self._data_handler.loading_error.connect(self._on_loading_error)
self._data_handler.loading_started.connect(self._on_loading_started)
self._data_handler.loading_progress.connect(self._on_loading_progress)
self._data_handler.loading_finished.connect(self._on_loading_finished)
self._data_handler.data_updated.connect(self._on_data_updated)
# PlotHandler → PlotCanvas
self._plot_handler.plot_ready.connect(self._on_plot_ready)
self._plot_handler.plot_error.connect(self._on_plot_error)
# Image-save feedback → status bar
self._plot_canvas.image_saved.connect(
lambda msg: self._status_bar.showMessage(msg, 5000)
)
# Debounced re-plot on canvas resize so aligned-mode x-axis tick labels
# re-thin to the actual rendered width (not a static estimate).
self._last_plot_width = 0
self._resize_replot_timer = QTimer(self)
self._resize_replot_timer.setSingleShot(True)
self._resize_replot_timer.setInterval(250)
self._resize_replot_timer.timeout.connect(self._replot_for_resize)
self._plot_canvas.resized.connect(
lambda w, h: self._resize_replot_timer.start()
)
self._topology_panel.image_saved.connect(
lambda msg: self._status_bar.showMessage(msg, 5000)
)
# Settings panel — auto-apply (P13)
self._settings_panel.settings_changed.connect(self._on_settings_changed)
self._settings_panel.customization_reset.connect(self._plot_handler.clear_customization)
# Topology — delegate handles launch + result dispatch
self._topology_delegate = TopologyDelegate(
self._data_handler, self._topology_handler, self._chat_handler,
self._topology_panel, self._chat_panel, self._central_stack,
self._status_bar, self,
)
self._topology_delegate.analysis_complete.connect(self._on_topology_complete)
self._topology_panel.topology_launch_requested.connect(self._topology_delegate.on_launch)
self._topology_handler.graph_ready.connect(self._topology_delegate.on_graph_ready)
self._topology_handler.chart_ready.connect(self._topology_delegate.on_chart_ready)
self._topology_handler.html_ready.connect(self._topology_delegate.on_html_ready)
self._topology_handler.analysis_error.connect(self._topology_delegate.on_error)
# Chat
self._chat_panel.message_sent.connect(self._chat_handler.ask)
self._chat_handler.response_ready.connect(self._chat_panel.show_response)
self._chat_handler.error.connect(lambda msg: self._chat_panel.show_response(f"Error: {msg}"))
self._chat_panel.set_llm_available(self._chat_handler.is_available)
# Wire data-panel action buttons
self._clear_graph_btn.clicked.connect(self._clear_graph)
self._data_panel._auto_update_btn.toggled.connect(self._auto_update_action.setChecked)
self._auto_update_action.toggled.connect(self._data_panel._auto_update_btn.setChecked)
self._data_panel._refresh_btn.clicked.connect(self._refresh_action.trigger)
# Auto-update delegate
self._auto_update_delegate = AutoUpdateDelegate(
self._data_handler, self._auto_update_action,
self._status_bar, parent=self,
)
self._auto_update_action.toggled.connect(self._auto_update_delegate.on_toggled)
self._refresh_action.triggered.connect(self._auto_update_delegate.manual_update)
# ==================================================================
# Slot Implementations
# ==================================================================
def _set_tab_enabled(self, index: int, enabled: bool) -> None:
"""Enable or disable an activity-bar button."""
btn = self._activity_btns[index]
btn.setEnabled(enabled)
if not enabled and btn.isChecked():
# Fall back to Data panel
self._on_activity_btn_clicked(_TAB_DATA)
def _on_activity_btn_clicked(self, index: int) -> None:
"""Switch the panel stack to the clicked activity button."""
if not self._activity_btns[index].isEnabled():
return
if index == _TAB_CHAT:
QMessageBox.information(
self, "AI Chat",
"The AI Chat feature is currently not implemented.\n"
"This functionality will be available in a future release."
)
return
for i, btn in enumerate(self._activity_btns):
btn.setChecked(i == index)
self._panel_stack.setCurrentIndex(index)
def _switch_to_panel(self, index: int) -> None:
"""Programmatically switch to a panel by index."""
self._on_activity_btn_clicked(index)
def _on_vis_chosen(self, vis_type: str) -> None:
"""Handle visualization type selection from the chooser panel."""
if vis_type == "topology":
QMessageBox.information(
self, "Topology Visualization",
"The Topology visualization feature is currently not available.\n"
"Please select Line Plot instead."
)
# Reset segmented control back to Line Plot
self._vis_segment.blockSignals(True)
self._vis_segment.setCurrentIndex(0)
self._vis_segment.blockSignals(False)
return
# Line Plot chosen
self._vis_type = vis_type
self._central_stack.setCurrentIndex(0) # show plot canvas
self._settings_stack.setCurrentIndex(1) # show plot settings
self._set_tab_enabled(_TAB_SETTINGS, True)
# Check if user already has plottable data selected
selected = self._data_panel.get_all_item_paths()
if selected:
# Trigger plot immediately with existing selection
self._on_selection_changed(selected)
else:
# No data selected yet — guide user to Data panel
self._switch_to_panel(_TAB_DATA)
QMessageBox.information(
self, "Line Plot Selected",
"Please select plottable quantities from the data tree\n"
"and add them to the selection list to generate a plot."
)
def _on_selection_changed(self, item_paths: list[str]) -> None:
"""P19: Auto-plot when selection changes. Update status bar."""
n = len(item_paths)
self._status_bar.showMessage(f"{n} item{'s' if n != 1 else ''} in selection list.")
# Only auto-plot if a visualization type has been chosen
if not self._vis_type:
return
if not item_paths:
# All items removed — clear the plot
if self._has_visualization:
self._clear_graph()
return
# Auto-plot
config = self._settings_panel.get_plot_config()
plottable_data = self._data_handler.get_plottable_data(item_paths)
if not plottable_data:
return
labels_map = self._data_handler.get_iteration_labels_map(item_paths)
self._sync_canvas_size()
self._plot_handler.create_plot(plottable_data, config, labels_map)
def _on_plot_ready(self, html: str) -> None:
"""Display the new plot HTML and refresh line entry controls."""
# Switch central stack first so the web view surface is visible
# before the deferred HTML injection
self._central_stack.setCurrentWidget(self._plot_canvas)
QApplication.processEvents() # Let compositor initialize the surface
self._plot_canvas.set_html(html, self._plot_handler.last_figure)
# Rebuild per-line/legend widgets when data items or ref lines change
new_keys = [p for p, _ in (self._plot_handler.last_plottable_data or [])]
old_keys = list(self._settings_panel._legend_entries.keys())
has_ref_lines = any(
w["enabled"].isChecked()
for w in self._settings_panel._ref_line_widgets
)
# Always rebuild when ref lines are active to keep labels in sync
if new_keys != old_keys or has_ref_lines:
self._settings_panel.refresh_line_entries(self._plot_handler.last_plottable_data)
# Update permanent status bar label
config = self._settings_panel.get_plot_config()
self._plot_mode_label.setText(f"Y: {config.plot_type} | X: {config.x_axis_type}")
self._status_bar.showMessage("Plot updated.")
# Unlock AI Chat once a visualization is generated
if not self._has_visualization:
self._has_visualization = True
self._set_tab_enabled(_TAB_CHAT, True)
self._clear_graph_btn.setEnabled(True)
def _sync_canvas_size(self) -> None:
size = self._plot_canvas.size()
width = max(400, size.width())
self._plot_handler.set_canvas_size(width, max(300, size.height()))
self._last_plot_width = width
def _replot_for_resize(self) -> None:
"""Re-plot after a resize so aligned-mode tick labels re-thin to width.
Only aligned x-axis modes compute tick-label density server-side, so
other modes (handled by Plotly's responsive rescaling) are skipped.
A minimum width delta avoids redundant re-plots during small drags.
"""
if not self._plot_handler.last_plottable_data:
return
config = self._settings_panel.get_plot_config()
if config.x_axis_type not in ("combined_ticks", "equispaced_major"):
return
new_width = max(400, self._plot_canvas.size().width())
if abs(new_width - self._last_plot_width) < 40:
return
labels_map = self._plot_handler.last_labels_map
item_paths = self._data_panel.get_all_item_paths()
if item_paths:
labels_map = self._data_handler.get_iteration_labels_map(item_paths)
self._sync_canvas_size()
self._plot_handler.create_plot(
self._plot_handler.last_plottable_data, config, labels_map,
)
def _on_plot_error(self, message: str) -> None:
self._status_bar.showMessage(f"Plot error: {message}")
QMessageBox.warning(self, "Plot Error", message)
def _on_topology_complete(self, analysis_type: str) -> None:
"""Handle topology delegate signalling analysis is done."""
self._has_visualization = True
self._set_tab_enabled(_TAB_CHAT, True)
self._switch_to_panel(_TAB_CHAT)
def _on_settings_changed(self) -> None:
"""Auto-apply: re-plot when ANY setting changes (P13)."""
if self._plot_handler.last_plottable_data:
config = self._settings_panel.get_plot_config()
labels_map = self._plot_handler.last_labels_map
if config.x_axis_type in ("cumulative_runtime", "cumulative_evaluations",
"combined_ticks", "equispaced_major"):
item_paths = self._data_panel.get_all_item_paths()
if item_paths:
labels_map = self._data_handler.get_iteration_labels_map(item_paths)
self._sync_canvas_size()
self._plot_handler.create_plot(
self._plot_handler.last_plottable_data, config, labels_map,
)
def _on_loading_error(self, filename: str, message: str) -> None:
self._status_bar.showMessage(f"Error loading {filename}: {message}")
def _on_loading_started(self, total: int) -> None:
self._progress_bar.setRange(0, total)
self._progress_bar.setValue(0)
self._progress_bar.setVisible(True)
self._status_bar.showMessage(f"Loading {total} file(s)...")
def _on_loading_progress(self, current: int, total: int) -> None:
self._progress_bar.setValue(current)
self._status_bar.showMessage(f"Loading file {current}/{total}...")
def _on_loading_finished(self) -> None:
self._progress_bar.setVisible(False)
n_entries = len(self._data_handler.list_entries())
self._file_count_label.setText(f"{n_entries} file{'s' if n_entries != 1 else ''}")
# Unlock Visualization tab after data is loaded
if n_entries > 0:
self._set_tab_enabled(_TAB_VIS, True)
self._data_panel._clear_all_btn.setEnabled(True)
self._data_panel._auto_update_btn.setEnabled(True)
self._data_panel._refresh_btn_base_enabled = True
self._data_panel._refresh_btn.setEnabled(
not self._data_panel._auto_update_btn.isChecked()
)
self._data_panel._hide_nonplottable_cb.setEnabled(True)
self._status_bar.showMessage("All files loaded.")
def _on_data_updated(self, entry_ids: list[str]) -> None:
"""Auto-update detected changes — replot if selections exist."""
item_paths = self._data_panel.get_all_item_paths()
if item_paths and self._plot_handler.last_plottable_data:
config = self._settings_panel.get_plot_config()
plottable_data = self._data_handler.get_plottable_data(item_paths)
labels_map = self._data_handler.get_iteration_labels_map(item_paths)
self._sync_canvas_size()
self._plot_handler.create_plot(plottable_data, config, labels_map)
self._status_bar.showMessage(f"Auto-update: {len(entry_ids)} file(s) changed, replotted.")
else:
self._status_bar.showMessage(f"Auto-update: {len(entry_ids)} file(s) changed.")
# ------------------------------------------------------------------
# Utility actions
# ------------------------------------------------------------------
def _clear_graph(self) -> None:
"""Clear the current visualization without removing loaded data."""
self._plot_canvas.clear()
self._plot_handler.clear()
self._plot_mode_label.setText("")
self._has_visualization = False
self._clear_graph_btn.setEnabled(False)
self._set_tab_enabled(_TAB_CHAT, False)
self._status_bar.showMessage("Graph cleared.")
def _clear_data(self) -> None:
self._data_handler.clear_all()
self._data_panel.clear_tree()
self._plot_canvas.clear()
self._topology_panel.clear()
self._plot_handler.clear()
self._file_count_label.setText("0 files")
self._plot_mode_label.setText("")
# Reset workflow state
self._vis_type = None
self._has_visualization = False
self._set_tab_enabled(_TAB_VIS, False)
self._set_tab_enabled(_TAB_SETTINGS, False)
self._set_tab_enabled(_TAB_CHAT, False)
self._settings_stack.setCurrentIndex(0)
self._switch_to_panel(_TAB_DATA)
# Reset button states
self._data_panel._clear_all_btn.setEnabled(False)
self._data_panel._auto_update_btn.setEnabled(False)
self._data_panel._refresh_btn_base_enabled = False
self._data_panel._refresh_btn.setEnabled(False)
self._data_panel._hide_nonplottable_cb.setEnabled(False)
self._clear_graph_btn.setEnabled(False)
# Reset vis segment (no signal)
self._vis_segment.blockSignals(True)
self._vis_segment.setCurrentIndex(0)
self._vis_segment.blockSignals(False)
self._status_bar.showMessage("All data cleared.")
def _show_about(self) -> None:
QMessageBox.about(
self, "About DDO Viewer",
"DDO Viewer — Distributed Optimization Processing\n\n"
"PySide6-based visualization tool for distributed design optimization results.\n"
"Supports .dill files with future PostgreSQL backend."
)
# ------------------------------------------------------------------
# State persistence
# ------------------------------------------------------------------
def closeEvent(self, event: QCloseEvent) -> None:
"""Persist window state and cancel background tasks before closing.
Args:
event: The close event.
"""
if self._auto_update_delegate:
self._auto_update_delegate.stop()
self._topology_handler.cancel()
self._chat_handler.cancel()
self._data_handler.cancel()
self._settings.setValue("chat_history", self._chat_panel.get_history_html())
self._settings.setValue("geometry", self.saveGeometry())
self._settings.setValue("windowState", self.saveState())
super().closeEvent(event)
def _restore_state(self) -> None:
geometry = self._settings.value("geometry")
if geometry:
self.restoreGeometry(geometry)
state = self._settings.value("windowState")
if state:
self.restoreState(state)
chat_html = self._settings.value("chat_history", "")
if chat_html:
self._chat_panel.restore_history(chat_html)