diff --git a/src/petab_gui/controllers/mother_controller.py b/src/petab_gui/controllers/mother_controller.py index ae8f253..6293bb5 100644 --- a/src/petab_gui/controllers/mother_controller.py +++ b/src/petab_gui/controllers/mother_controller.py @@ -42,6 +42,7 @@ process_file, ) from ..views import TaskBar +from ..views.other_views import NextStepsPanel from .logger_controller import LoggerController from .sbml_controller import SbmlController from .table_controllers import ( @@ -150,6 +151,11 @@ def __init__(self, view, model: PEtabModel): } self.sbml_checkbox_states = {"sbml": False, "antimony": False} self.unsaved_changes = False + # Next Steps Panel + self.next_steps_panel = NextStepsPanel(self.view) + self.next_steps_panel.dont_show_again_changed.connect( + self._handle_next_steps_dont_show_again + ) self.filter = QLineEdit() self.filter_active = {} # Saves which tables the filter applies to self.actions = self.setup_actions() @@ -509,6 +515,12 @@ def setup_actions(self): ) ) + # Show next steps panel action + actions["next_steps"] = QAction( + qta.icon("mdi6.lightbulb-on"), "Possible next steps...", self.view + ) + actions["next_steps"].triggered.connect(self._show_next_steps_panel) + # Undo / Redo actions["undo"] = QAction(qta.icon("mdi6.undo"), "&Undo", self.view) actions["undo"].setShortcut(QKeySequence.Undo) @@ -612,6 +624,14 @@ def save_model(self): "Save Project", f"Project saved successfully to {file_name}", ) + + # Show next steps panel if not disabled + dont_show = settings_manager.get_value( + "next_steps/dont_show_again", False, bool + ) + if not dont_show: + self.next_steps_panel.show_panel() + return True def save_single_table(self): @@ -1502,6 +1522,26 @@ def about(self): f"{config_file}", ) + def _show_next_steps_panel(self): + """Show the next steps panel (ignores 'don't show again' preference).""" + # Sync checkbox state with current settings + dont_show = settings_manager.get_value( + "next_steps/dont_show_again", False, bool + ) + self.next_steps_panel.set_dont_show_again(dont_show) + self.next_steps_panel.show_panel() + + def _handle_next_steps_dont_show_again(self, dont_show: bool): + """Handle the 'don't show again' checkbox state change. + + Connected to the next steps panel's dont_show_again_changed signal. + Persists the user's preference to settings. + + Args: + dont_show: Whether to suppress the panel on future saves + """ + settings_manager.set_value("next_steps/dont_show_again", dont_show) + def get_current_problem(self): """Get the current PEtab problem from the model.""" return self.model.current_petab_problem diff --git a/src/petab_gui/views/other_views.py b/src/petab_gui/views/other_views.py index cb5480b..f75561f 100644 --- a/src/petab_gui/views/other_views.py +++ b/src/petab_gui/views/other_views.py @@ -1,12 +1,16 @@ """Collection of other views aside from the main ones.""" +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( + QCheckBox, QComboBox, QDialog, + QFrame, QHBoxLayout, QLabel, QLineEdit, QPushButton, + QTextBrowser, QVBoxLayout, ) @@ -59,8 +63,273 @@ def __init__( btns.addWidget(ok) lay.addLayout(btns) - def get_result(self) -> tuple[str | None, str | None]: + def get_result(self) -> tuple[str | None, str | None, str]: dose = self._dose.currentText() or None time_text = (self._time.text() or "").strip() or None preeq = (self._preeq_edit.text() or "").strip() return dose, time_text, preeq + + +class NextStepsPanel(QDialog): + """Non-modal panel showing possible next steps after saving.""" + + dont_show_again_changed = Signal(bool) + + # Styling constants + MIN_WIDTH = 450 + MAX_WIDTH = 600 + MIN_HEIGHT = 360 + FRAME_PADDING = 8 + FRAME_BORDER_RADIUS = 4 + LAYOUT_MARGIN = 12 + LAYOUT_SPACING = 10 + + # Card background colors + COLOR_BENCHMARK = "rgba(255, 193, 7, 0.08)" + COLOR_PYPESTO = "rgba(100, 149, 237, 0.08)" + COLOR_COPASI = "rgba(169, 169, 169, 0.08)" + COLOR_OTHER_TOOLS = "rgba(144, 238, 144, 0.08)" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Possible next steps") + self.setModal(False) + self.setMinimumWidth(self.MIN_WIDTH) + self.setMaximumWidth(self.MAX_WIDTH) + self.setMinimumHeight(self.MIN_HEIGHT) + + # Main layout + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins( + self.LAYOUT_MARGIN, + self.LAYOUT_MARGIN, + self.LAYOUT_MARGIN, + self.LAYOUT_MARGIN, + ) + main_layout.setSpacing(self.LAYOUT_SPACING) + + # Description + desc = QLabel( + "This parameter estimation problem can now be used in the following tools:" + ) + desc.setWordWrap(True) + main_layout.addWidget(desc) + + # Main suggestions + suggestions_layout = QVBoxLayout() + suggestions_layout.setSpacing(8) + + # Benchmark Collection action + benchmark_frame = self._create_tool_card( + bg_color=self.COLOR_BENCHMARK, + html_content=( + '

' + "📚 Contribute to Benchmark Collection
" + "Share your publsihed PEtab problem with the community to " + "validate it, enable reproducibility, and support " + "benchmarking.
" + 'Benchmark Collection

' + ), + ) + suggestions_layout.addWidget(benchmark_frame) + + # pyPESTO action + pypesto_frame = self._create_tool_card( + bg_color=self.COLOR_PYPESTO, + html_content=( + '

' + "â–¶ Parameter Estimation with pyPESTO
" + "Use pyPESTO for parameter estimation, uncertainty analysis, " + "and model selection.
" + 'pyPESTO documentation

' + ), + ) + suggestions_layout.addWidget(pypesto_frame) + + # COPASI action + copasi_frame = self._create_tool_card( + bg_color=self.COLOR_COPASI, + html_content=( + '

' + "âš™ Advanced Model Adaptation and Simulation
" + "Use COPASI for further model adjustment and advanced " + "simulation with a graphical interface.
" + 'COPASI website

' + ), + ) + suggestions_layout.addWidget(copasi_frame) + + main_layout.addLayout(suggestions_layout) + + # Collapsible section for other tools + self._other_tools_btn = QPushButton( + "📊 ▶ Other tools supporting PEtab" + ) + self._other_tools_btn.setCheckable(True) + self._other_tools_btn.setFlat(True) + self._other_tools_btn.setStyleSheet( + "QPushButton { text-align: left; padding: 6px; " + "font-weight: normal; }" + "QPushButton:checked { font-weight: bold; }" + ) + self._other_tools_btn.clicked.connect(self._toggle_other_tools) + main_layout.addWidget(self._other_tools_btn) + + # Other tools frame (initially hidden) + self._other_tools_frame = QFrame() + self._other_tools_frame.setStyleSheet( + f"QFrame {{ background-color: {self.COLOR_OTHER_TOOLS}; " + f"border-radius: {self.FRAME_BORDER_RADIUS}px; " + f"padding: {self.FRAME_PADDING}px; }}" + ) + self._other_tools_frame.setVisible(False) + other_tools_layout = QVBoxLayout(self._other_tools_frame) + other_tools_layout.setContentsMargins( + self.FRAME_PADDING, + self.FRAME_PADDING, + self.FRAME_PADDING, + self.FRAME_PADDING, + ) + other_tools_layout.setSpacing(4) + + # Framing text + framing_text = QLabel("Additional tools in the PEtab ecosystem:") + framing_text.setWordWrap(True) + other_tools_layout.addWidget(framing_text) + + other_tools_text = QTextBrowser() + other_tools_text.setOpenExternalLinks(True) + other_tools_text.setMaximumHeight(120) + other_tools_text.setFrameStyle(QFrame.NoFrame) + other_tools_text.setStyleSheet( + "QTextBrowser { background: transparent; }" + ) + other_tools_text.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAsNeeded + ) + other_tools_text.setHtml( + '" + ) + other_tools_layout.addWidget(other_tools_text) + + main_layout.addWidget(self._other_tools_frame) + + # Spacer + main_layout.addStretch() + + # Reassurance text + reassurance = QLabel( + "You can always access this dialog from the " + "Help menu." + ) + reassurance.setWordWrap(True) + reassurance.setStyleSheet("QLabel { color: gray; padding: 0; }") + main_layout.addWidget(reassurance) + + # Bottom section with checkbox and close button + bottom_layout = QHBoxLayout() + bottom_layout.setSpacing(8) + + self._dont_show_checkbox = QCheckBox("Don't show after saving") + self._dont_show_checkbox.toggled.connect( + self.dont_show_again_changed.emit + ) + bottom_layout.addWidget(self._dont_show_checkbox) + + bottom_layout.addStretch() + + close_btn = QPushButton("Close") + close_btn.clicked.connect(self.close) + close_btn.setDefault(True) + bottom_layout.addWidget(close_btn) + + main_layout.addLayout(bottom_layout) + + def _create_tool_card( + self, bg_color: str, html_content: str, scrollbar_policy=None + ) -> QFrame: + """Create a styled card for displaying tool information. + + Args: + bg_color: Background color for the frame (rgba string) + html_content: HTML content to display in the text browser + scrollbar_policy: Optional scrollbar policy (defaults to AlwaysOff) + + Returns: + Configured QFrame containing the tool information + """ + frame = QFrame() + frame.setStyleSheet( + f"QFrame {{ background-color: {bg_color}; " + f"border-radius: {self.FRAME_BORDER_RADIUS}px; " + f"padding: {self.FRAME_PADDING}px; }}" + ) + layout = QVBoxLayout(frame) + layout.setContentsMargins( + self.FRAME_PADDING, + self.FRAME_PADDING, + self.FRAME_PADDING, + self.FRAME_PADDING, + ) + layout.setSpacing(4) + + text_browser = QTextBrowser() + text_browser.setOpenExternalLinks(True) + text_browser.setFrameStyle(QFrame.NoFrame) + text_browser.setStyleSheet("QTextBrowser { background: transparent; }") + if scrollbar_policy is None: + scrollbar_policy = Qt.ScrollBarPolicy.ScrollBarAlwaysOff + text_browser.setVerticalScrollBarPolicy(scrollbar_policy) + text_browser.setHtml(html_content) + layout.addWidget(text_browser) + + return frame + + def _toggle_other_tools(self, checked): + """Toggle visibility of other tools section.""" + self._other_tools_frame.setVisible(checked) + # Update button text to show expand/collapse state + arrow = "▼" if checked else "▶" + icon = "📊" + self._other_tools_btn.setText( + f"{icon} {arrow} Other tools supporting PEtab" + ) + # Adjust window size + self.adjustSize() + + def set_dont_show_again(self, dont_show: bool): + """Set the 'don't show again' checkbox state.""" + self._dont_show_checkbox.setChecked(dont_show) + + def show_panel(self): + """Show the panel and center it on the parent.""" + if self.parent(): + # Center on parent window + parent_geo = self.parent().geometry() + self.move( + parent_geo.center().x() - self.width() // 2, + parent_geo.center().y() - self.height() // 2, + ) + self.show() + self.raise_() + self.activateWindow() diff --git a/src/petab_gui/views/task_bar.py b/src/petab_gui/views/task_bar.py index 3bc6a61..6c4e8b1 100644 --- a/src/petab_gui/views/task_bar.py +++ b/src/petab_gui/views/task_bar.py @@ -139,6 +139,8 @@ def __init__(self, parent, actions): # Add actions to the menu for re-adding tables self.menu.addAction(actions["open_documentation"]) + self.menu.addAction(actions["next_steps"]) + self.menu.addSeparator() self.menu.addAction(actions["whats_this"]) self.menu.addAction(actions["about"])