diff --git a/src/qus/analysis_loading/analysis_loading_controller.py b/src/qus/analysis_loading/analysis_loading_controller.py index 21a69e3..c0eab4e 100644 --- a/src/qus/analysis_loading/analysis_loading_controller.py +++ b/src/qus/analysis_loading/analysis_loading_controller.py @@ -65,20 +65,7 @@ def _setup_analysis_options(self) -> None: """Setup available analysis types and functions in the view.""" analysis_types, analysis_functions = self._model.get_analysis_types() - # Automatically select "Paramap" as the analysis type - paramap_type = "paramap" - if paramap_type in analysis_types: - self._selected_analysis_type = paramap_type - if self._model.set_analysis_type(paramap_type): - # Get available functions for Paramap analysis - available_functions = self._model.get_analysis_functions( - paramap_type, self._image_data.spatial_dims) - # Skip analysis type selection and go directly to function selection - self._view_coordinator.show_function_selection(available_functions) - else: - self._view_coordinator.show_error("Failed to set Paramap analysis type") - else: - self._view_coordinator.show_error("Paramap analysis type not available") + self._view_coordinator.show_analysis_type_selection(analysis_types) def _on_user_action(self, action_name: str, action_data: Any) -> None: """ @@ -89,7 +76,9 @@ def _on_user_action(self, action_name: str, action_data: Any) -> None: action_data: Data associated with the action """ - if action_name == "analysis_functions_selected": + if action_name == "analysis_type_selected": + self._handle_analysis_type_selection(action_data) + elif action_name == "analysis_functions_selected": self._handle_analysis_functions_selection(action_data) elif action_name == "analysis_execution_started": self._handle_analysis_execution(action_data) @@ -150,6 +139,28 @@ def _on_analysis_error(self, error_message: str) -> None: self._view_coordinator.show_error(error_message) self._analysis_running = False + def _handle_analysis_type_selection(self, selected_type: str) -> None: + """ + Handle analysis type selection. + + Args: + selected_type: String containing selected analysis type + """ + self._selected_analysis_type = selected_type + + if not self._model.set_analysis_type(selected_type): + self._view_coordinator.show_error( + f"Failed to set analysis type: {selected_type}" + ) + return + + available_functions = self._model.get_analysis_functions( + selected_type, + self._image_data.spatial_dims + ) + + self._view_coordinator.show_function_selection(available_functions) + def _handle_analysis_functions_selection(self, selected_func_names: List[str]) -> None: """ Handle analysis functions selection. diff --git a/src/qus/analysis_loading/analysis_loading_view_coordinator.py b/src/qus/analysis_loading/analysis_loading_view_coordinator.py index 7f7f48a..25e5a0a 100644 --- a/src/qus/analysis_loading/analysis_loading_view_coordinator.py +++ b/src/qus/analysis_loading/analysis_loading_view_coordinator.py @@ -13,6 +13,7 @@ from src.qus.mvc.base_view import BaseViewMixin from .views.analysis_function_selection_widget import AnalysisFunctionSelectionWidget from .views.analysis_params_widget import AnalysisParamsWidget +from .views.analysis_type_selection_widget import AnalysisTypeSelectionWidget from engines.qus.quantus.data_objs import UltrasoundRfImage, BmodeSeg, RfAnalysisConfig from engines.qus.quantus.analysis.paramap.framework import ParamapAnalysis @@ -49,9 +50,7 @@ def __init__(self, image_data: UltrasoundRfImage, seg_data: BmodeSeg, config_dat # Widget instances self._function_selection_widget: Optional[AnalysisFunctionSelectionWidget] = None self._params_widget: Optional[AnalysisParamsWidget] = None - - # Note: Analysis type selection is now skipped - Paramap is automatically selected - # The controller will call show_function_selection directly + self._type_selection_widget: Optional[AnalysisTypeSelectionWidget] = None # ============================================================================ # CONTROLLER INPUT ROUTING - Route inputs from controller to appropriate widget @@ -102,6 +101,27 @@ def clear_error(self) -> None: # NAVIGATION METHODS - Methods to show different widgets # ============================================================================ + def show_analysis_type_selection(self, analysis_types: Dict) -> None: + """ + Show the analysis type selection widget. + + Args: + analysis_types: Dictionary of analysis types + """ + if self._type_selection_widget is None: + self._type_selection_widget = AnalysisTypeSelectionWidget( + self._image_data, + analysis_types + ) + self._type_selection_widget.analysis_type_selected.connect(self._on_analysis_type_selected) + self._type_selection_widget.back_requested.connect(self.back_requested.emit) + self._type_selection_widget.close_requested.connect(self.close_requested.emit) + + self.addWidget(self._type_selection_widget) + + self.setCurrentWidget(self._type_selection_widget) + self._type_selection_widget.clear_error() + def show_function_selection(self, available_functions: Dict) -> None: """ Show the analysis function selection widget. @@ -116,6 +136,8 @@ def show_function_selection(self, available_functions: Dict) -> None: self._function_selection_widget.back_requested.connect(self._on_function_selection_back) self._function_selection_widget.close_requested.connect(self.close_requested.emit) self.addWidget(self._function_selection_widget) + else: + self._function_selection_widget.set_functions(available_functions) self.setCurrentWidget(self._function_selection_widget) self._function_selection_widget.clear_error() @@ -142,6 +164,15 @@ def show_params_configuration(self, required_params: Dict[str, Dict[str, str]], # EVENT HANDLERS - Handle events from child widgets # ============================================================================ + def _on_analysis_type_selected(self, selected_type: str) -> None: + """ + Handle analysis type selection. + + Args: + selected_type: String containing selected analysis type + """ + self._emit_user_action("analysis_type_selected", selected_type) + def _on_functions_selected(self, selected_func_names: List[str]) -> None: """ Handle analysis functions selection. @@ -162,8 +193,9 @@ def _on_params_configured(self, params: dict) -> None: def _on_function_selection_back(self) -> None: """Handle back navigation from function selection.""" - # Since we skip analysis type selection, go back to the main application flow - self.back_requested.emit() + # Go back to type selection + if self._type_selection_widget: + self.setCurrentWidget(self._type_selection_widget) def _on_params_back(self) -> None: """Handle back navigation from parameters configuration.""" diff --git a/src/qus/analysis_loading/ui/analysis_type_selection.ui b/src/qus/analysis_loading/ui/analysis_type_selection.ui new file mode 100644 index 0000000..be0c4cc --- /dev/null +++ b/src/qus/analysis_loading/ui/analysis_type_selection.ui @@ -0,0 +1,653 @@ + + + analysisTypeSelection + + + + 0 + 0 + 1512 + 832 + + + + + 201 + 31 + + + + + 16777215 + 16777215 + + + + Select Ultrasound Image + + + QWidget { + background: rgb(42, 42, 42); +} + + + + + 70 + 10 + 1545 + 844 + + + + + + + 0 + + + QLayout::SizeConstraint::SetMaximumSize + + + + + + 341 + 601 + + + + + 241 + 601 + + + + <html><head/><body><p><br/></p></body></html> + + + QWidget { + background-color: rgb(28, 0, 101); +} + + + + + 0 + 0 + 341 + 121 + + + + + 341 + 121 + + + + + 341 + 121 + + + + QFrame { + background-color: rgb(99, 0, 174); + border: 1px solid black; +} + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + 70 + 0 + 191 + 51 + + + + QLabel { + font-size: 21px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; + font-weight: bold; +} + + + Image Selection: + + + Qt::AlignmentFlag::AlignCenter + + + + + + -60 + 40 + 191 + 51 + + + + QLabel { + font-size: 16px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; + font-weight: bold; +} + + + Image: + + + Qt::AlignmentFlag::AlignCenter + + + + + + -50 + 70 + 191 + 51 + + + + QLabel { + font-size: 16px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; + font-weight: bold +} + + + Phantom: + + + Qt::AlignmentFlag::AlignCenter + + + + + + 100 + 40 + 241 + 51 + + + + QLabel { + font-size: 14px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; +} + + + TextLabel + + + + + + 100 + 70 + 241 + 51 + + + + QLabel { + font-size: 14px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; +} + + + TextLabel + + + + + + + 0 + 120 + 341 + 121 + + + + + 341 + 121 + + + + QFrame { + background-color: rgb(99, 0, 174); + border: 1px solid black; +} + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + 0 + 40 + 341 + 51 + + + + QLabel { + font-size: 21px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; + font-weight: bold; +} + + + Segmentation Selection + + + Qt::AlignmentFlag::AlignCenter + + + + + + + 0 + 360 + 341 + 121 + + + + + 341 + 121 + + + + + 341 + 121 + + + + QFrame { + background-color: rgb(99, 0, 174); + border: 1px solid black; +} + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + 0 + 30 + 341 + 51 + + + + QLabel { + font-size: 21px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; + font-weight: bold; +} + + + QUS Analysis + + + Qt::AlignmentFlag::AlignCenter + + + + + + + 0 + 480 + 341 + 121 + + + + + 341 + 121 + + + + + 341 + 121 + + + + QFrame { + background-color: rgb(49, 0, 124); + border: 1px solid black; +} + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + 20 + 30 + 301 + 51 + + + + QLabel { + font-size: 21px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; + font-weight: bold; +} + + + Visualization / Export + + + Qt::AlignmentFlag::AlignCenter + + + + + + + 0 + 240 + 341 + 121 + + + + + 341 + 121 + + + + QFrame { + background-color: rgb(99, 0, 174); + border: 1px solid black; +} + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + 0 + 30 + 341 + 51 + + + + QLabel { + font-size: 21px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); + border: 0px; + font-weight:bold; +} + + + Analysis Parameter Selection + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + + 341 + 0 + + + + + 341 + 16777215 + + + + QFrame { + background-color: rgb(28, 0, 101); +} + + + + QLayout::SizeConstraint::SetMinAndMaxSize + + + 10 + + + 10 + + + 10 + + + 10 + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + 131 + 41 + + + + + 131 + 41 + + + + QPushButton { + color: white; + font-size: 16px; + background: rgb(90, 37, 255); + border-radius: 15px; +} + + + Back + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 50 + + + 30 + + + 30 + + + + + QLabel { + font-size: 29px; + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0); +} + + + Select Analysis Type: + + + Qt::TextFormat::AutoText + + + false + + + Qt::AlignmentFlag::AlignCenter + + + true + + + + + + + + 180 + 41 + + + + + 16777215 + 16777215 + + + + + 16 + + + + QComboBox { + color: white; +} + + + + + + + + 131 + 41 + + + + + 131 + 41 + + + + QPushButton { + color: white; + font-size: 16px; + background: rgb(90, 37, 255); + border-radius: 15px; +} + + + Next + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + diff --git a/src/qus/analysis_loading/views/analysis_function_selection_widget.py b/src/qus/analysis_loading/views/analysis_function_selection_widget.py index d92e930..4287103 100644 --- a/src/qus/analysis_loading/views/analysis_function_selection_widget.py +++ b/src/qus/analysis_loading/views/analysis_function_selection_widget.py @@ -55,7 +55,13 @@ def _setup_ui(self) -> None: self._ui.image_path_input.setText(self._image_data.scan_name) self._ui.phantom_path_input.setText(self._image_data.phantom_name) - # Update available functions + self.set_functions(self._func_names) + + def set_functions(self, func_names: List[str]) -> None: + """Update available functions.""" + self._func_names = func_names + self._ui.funcs_list.clear() + for func_name in self._func_names: if func_name == "compute_power_spectra": continue # skip this function for now diff --git a/src/qus/analysis_loading/views/analysis_type_selection_widget.py b/src/qus/analysis_loading/views/analysis_type_selection_widget.py new file mode 100644 index 0000000..7958925 --- /dev/null +++ b/src/qus/analysis_loading/views/analysis_type_selection_widget.py @@ -0,0 +1,78 @@ +""" +Analysis Type Selection Widget for Analysis Loading + +This widget allows users to select which analysis type to run. +It provides a dropdown menu for analysis type options. +""" + +from typing import Optional +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtWidgets import QWidget, QMessageBox + +from src.qus.mvc.base_view import BaseViewMixin +from src.qus.analysis_loading.ui.analysis_type_selection_ui import Ui_analysisTypeSelection +from engines.qus.quantus.data_objs import UltrasoundRfImage + +class AnalysisTypeSelectionWidget(QWidget, BaseViewMixin): + """ + Widget for selecting which analysis type to select functions from. + + This widget displays available analysis types in a dropdown menu. + """ + + # Signals for communicating with controller + analysis_type_selected = pyqtSignal(str) + close_requested = pyqtSignal() + back_requested = pyqtSignal() + + def __init__(self, image_data: UltrasoundRfImage, analysis_types: dict, parent: Optional[QWidget] = None): + QWidget.__init__(self, parent) + self.__init_base_view__(parent) + self._ui = Ui_analysisTypeSelection() + self._image_data = image_data + + self._setup_ui() + self._connect_signals() + self._set_type_options(analysis_types) + + def _setup_ui(self) -> None: + """Setup the user interface.""" + self._ui.setupUi(self) + + # Configure layout for type selection + self.setLayout(self._ui.full_screen_layout) + + # Configure stretch factors + self._ui.full_screen_layout.setStretchFactor(self._ui.side_bar_layout, 1) + self._ui.full_screen_layout.setStretchFactor(self._ui.analysis_type_layout, 10) + + # Update image and phantom paths + self._ui.image_path_input.setText(self._image_data.scan_name) + self._ui.phantom_path_input.setText(self._image_data.phantom_name) + + def _connect_signals(self) -> None: + """Connect UI signals to internal handlers.""" + self._ui.next_button.clicked.connect(self._on_next_clicked) + self._ui.back_button.clicked.connect(self._on_back_clicked) + + def _on_next_clicked(self) -> None: + """Handle next button click.""" + selected_type = self._ui.analysis_type_options.currentText() + if selected_type: + self.analysis_type_selected.emit(selected_type) + else: + QMessageBox.critical(self, "Error", "Please select an analysis type.") + + def _on_back_clicked(self) -> None: + """Handle back button click.""" + self.back_requested.emit() + + def _set_type_options(self, analysis_types: dict) -> None: + """ + Set available analysis types in the dropdown. + + Args: + analysis_types: Dict + """ + self._ui.analysis_type_options.clear() + self._ui.analysis_type_options.addItems(analysis_types)