Source code for sec_interp.gui.main_dialog

from __future__ import annotations

# /***************************************************************************
# SecInterpDialog
#                                 A QGIS plugin
# Data extraction for geological interpretation
# Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
#                             -------------------
#        begin                : 2025-11-15
#        git sha              : $Format:%H$
#        copyright            : (C) 2025 by Juan M Bernales
#        email                : juanbernales@gmail.com
# ***************************************************************************/
#
# /***************************************************************************
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU General Public License as published by  *
# *   the Free Software Foundation; either version 2 of the License, or     *
# *   (at your option) any later version.                                   *
# *                                                                         *
# ***************************************************************************/

"""Main Dialog Module.

Contains the SecInterpDialog class which is the primary UI for the plugin.
"""

from pathlib import Path
from typing import Any

from qgis.core import (
    Qgis,
    QgsProject,
)
from qgis.PyQt.QtCore import QUrl
from qgis.PyQt.QtGui import QDesktopServices
from qgis.PyQt.QtWidgets import (
    QDialogButtonBox,
    QPushButton,
)

from sec_interp.core.types import InterpretationPolygon
from sec_interp.gui.utils import show_user_message
from sec_interp.logger_config import get_logger


class _NoOpMessageBar:
    """Safe no-op messagebar when iface is not available."""

    def pushMessage(self, *_args, **_kwargs):
        """No-op implementation of pushMessage."""
        return None


from .legend_widget import LegendWidget

logger = get_logger(__name__)
from .main_dialog_cache_handler import CacheHandler
from .main_dialog_data import DialogDataAggregator
from .main_dialog_export import ExportManager
from .main_dialog_interpretation import DialogInterpretationManager
from .main_dialog_messages import MessageManager
from .main_dialog_preview import PreviewManager
from .main_dialog_settings import DialogSettingsManager
from .main_dialog_signals import DialogSignalManager
from .main_dialog_status import DialogStatusManager
from .main_dialog_tools import DialogToolManager, NavigationManager
from .main_dialog_utils import DialogEntityManager
from .main_dialog_validation_manager import DialogValidationManager
from .preview_layer_factory import PreviewLayerFactory
from .ui.main_window import SecInterpMainWindow


[docs] class SecInterpDialog(SecInterpMainWindow): """Dialog for the SecInterp QGIS plugin. This dialog provides the user interface and helper methods to populate combo boxes with layers from the current QGIS project (raster and vector layers filtered by geometry type). It also exposes the interface and plugin instance for interaction with the host application. Attributes: iface (QgsInterface): The QGIS interface instance. plugin_instance (SecInterp): The plugin instance that created this dialog. messagebar (QgsMessageBar): The message bar widget for notifications. """
[docs] def __init__(self, iface=None, plugin_instance=None, parent=None): """Initialize the dialog.""" # Initialize the base class which sets up the programmatic UI super().__init__(iface, parent) self.iface = iface self.plugin_instance = plugin_instance self.project = QgsProject.instance() # Provide a safe, no-op messagebar when iface is not available (tests) if self.iface is None: self.messagebar = _NoOpMessageBar() else: self.messagebar = self.iface.messageBar() # Initialize manager instances self._init_managers() # Create legend widget self.legend_widget = LegendWidget(self.preview_widget.canvas) # Store current preview data self.current_topo_data = None self.current_geol_data = None self.current_struct_data = None self.current_canvas = None self.current_layers = [] # Interpretations list is now managed by interpretation_manager # Note: interpretation_manager is initialized later in _init_managers # Add cache and reset buttons self.clear_cache_btn = QPushButton(self.tr("Clear Cache")) self.clear_cache_btn.setToolTip(self.tr("Clear cached data to force re-processing.")) self.button_box.addButton(self.clear_cache_btn, QDialogButtonBox.ActionRole) self.reset_defaults_btn = QPushButton(self.tr("Reset Defaults")) self.reset_defaults_btn.setToolTip(self.tr("Reset all inputs to their default values.")) self.button_box.addButton(self.reset_defaults_btn, QDialogButtonBox.ActionRole) # Initialize map tools via tool_manager self.tool_manager.initialize_tools() # Connect all signals self.signal_manager = DialogSignalManager(self) self.signal_manager.connect_all() # Connect extra tool buttons self.clear_cache_btn.clicked.connect(self.clear_cache_handler) self.reset_defaults_btn.clicked.connect(self.reset_defaults_handler) # Initial state update # Initial state update self.status_manager.update_all() self.settings_manager.load_settings() # Flag to control saving settings on close self._save_on_close = True
def _init_managers(self): """Initialize all manager instances.""" from sec_interp.core.services.preview_service import PreviewService self.message_manager = MessageManager(self) self.validation_manager = DialogValidationManager(self) self.preview_manager = PreviewManager(self, PreviewService(self.plugin_instance.controller)) self.export_manager = ExportManager(self) self.cache_handler = CacheHandler(self) self.data_aggregator = DialogDataAggregator(self) self.settings_manager = DialogSettingsManager(self) self.status_manager = DialogStatusManager(self) self.status_manager.setup_indicators() self.interpretation_manager = DialogInterpretationManager(self) self.interpretation_manager.load_interpretations() self.tool_manager = DialogToolManager(self) self.navigation_manager = NavigationManager(self) self.layer_factory = PreviewLayerFactory()
[docs] def handle_error(self, error: Exception, title: str = "Error"): """Centralized error handling for the dialog. Args: error: The exception to handle. title: Title for the error message box. """ self.message_manager.handle_error(error, title)
[docs] def wheelEvent(self, event: Any) -> None: """Handle mouse wheel for zooming in preview via navigation_manager.""" if self.navigation_manager.handle_wheel_event(event): return super().wheelEvent(event)
[docs] def closeEvent(self, event: Any) -> None: """Handle dialog close event to clean up resources.""" if self._save_on_close: self.settings_manager.save_settings() logger.info("Closing dialog, cleaning up resources...") self.interpretation_manager.save_interpretations() self.preview_manager.cleanup() super().closeEvent(event)
[docs] def open_help(self): """Open the help file in the default browser.""" # Fix: help is at project root, main_dialog is in gui/ help_file = Path(__file__).parent.parent / "help" / "html" / "index.html" if help_file.exists(): QDesktopServices.openUrl(QUrl.fromLocalFile(str(help_file))) else: self.message_manager.push_message( self.tr("Error"), self.tr("Help file not found. Please run 'make doc' to generate it."), level=Qgis.Warning, )
[docs] def toggle_measure_tool(self, checked: bool) -> None: """Toggle measurement tool via tool_manager.""" self.tool_manager.toggle_measure_tool(checked)
[docs] def update_measurement_display(self, metrics: dict[str, Any]) -> None: """Display measurement results from multi-point tool via tool_manager.""" self.tool_manager.update_measurement_display(metrics)
[docs] def toggle_interpretation_tool(self, checked: bool) -> None: """Toggle interpretation tool via tool_manager.""" self.tool_manager.toggle_interpretation_tool(checked)
[docs] def on_interpretation_finished(self, interpretation: InterpretationPolygon) -> None: """Handle finalized interpretation polygon.""" self.interpretation_manager.handle_interpretation_finished(interpretation)
@property def interpretations(self): """Proxy to interpretations in the manager for backward compatibility.""" return self.interpretation_manager.interpretations @interpretations.setter def interpretations(self, value): self.interpretation_manager.interpretations = value
[docs] def update_preview_checkbox_states(self): """Enable or disable preview checkboxes via status_manager.""" self.status_manager.update_preview_checkbox_states()
[docs] def update_button_state(self): """Enable or disable buttons via status_manager.""" self.status_manager.update_button_state()
[docs] def get_selected_values(self): """Get the selected values from the dialog. Returns: Dictionary with all dialog values in legacy flat format """ return self.data_aggregator.get_all_values()
[docs] def get_preview_options(self): """Return the state of preview layer checkboxes. Returns: dict: Keys 'show_topo', 'show_geol', 'show_struct' with boolean values. """ return { "show_topo": bool(self.preview_widget.chk_topo.isChecked()), "show_geol": bool(self.preview_widget.chk_geol.isChecked()), "show_struct": bool(self.preview_widget.chk_struct.isChecked()), "show_drillholes": bool(self.preview_widget.chk_drillholes.isChecked()), "show_interpretations": bool(self.preview_widget.chk_interpretations.isChecked()), "show_legend": bool(self.preview_widget.chk_legend.isChecked()), "max_points": self.preview_widget.spin_max_points.value(), "auto_lod": self.preview_widget.chk_auto_lod.isChecked(), "use_adaptive_sampling": bool(self.preview_widget.chk_adaptive_sampling.isChecked()), }
[docs] def update_preview_from_checkboxes(self): """Update preview when checkboxes change. This method delegates to PreviewManager for preview updates. """ self.preview_manager.update_from_checkboxes()
[docs] def preview_profile_handler(self): """Generate a quick preview with topographic, geological, and structural data. This method delegates to PreviewManager for preview generation. """ success, message = self.preview_manager.generate_preview() if success: # Auto-save settings on successful preview self.settings_manager.save_settings() if not success and message: self.message_manager.push_message(self.tr("Preview Error"), message, level=Qgis.Warning)
[docs] def export_preview(self): """Export the current preview to a file using ExportManager.""" self.export_manager.export_preview()
[docs] def accept_handler(self): """Handle the accept button click event.""" # Proactively save settings as UI state, even if validation fails self.settings_manager.save_settings() # When running without a QGIS iface (tests), skip strict validation if self.iface is None: self.accept() return if not self.validate_inputs(): return self.interpretation_manager.save_interpretations() self.accept()
[docs] def reject_handler(self): """Handle the reject button click event.""" self._save_on_close = False self.close()
[docs] def validate_inputs(self): """Validate the inputs from the dialog. This method delegates to DialogValidationManager for input validation. """ is_valid, error_message = self.validation_manager.validate_inputs() if not is_valid: show_user_message(self, self.tr("Validation Error"), error_message) return is_valid
[docs] def clear_cache_handler(self): """Clear cached data via CacheHandler.""" self.cache_handler.clear_cache()
[docs] def reset_defaults_handler(self): """Reset all dialog inputs via settings_manager.""" self.settings_manager.reset_to_defaults()
def _populate_field_combobox(self, source_combobox: Any, target_combobox: Any) -> None: """Populate a combobox with field names.""" DialogEntityManager.populate_field_combobox(source_combobox, target_combobox)
[docs] def get_layer_names_by_type(self, layer_type) -> list[str]: """Get layer names by type.""" return DialogEntityManager.get_layer_names_by_type(layer_type)
[docs] def get_layer_names_by_geometry(self, geometry_type) -> list[str]: """Get layer names by geometry.""" return DialogEntityManager.get_layer_names_by_geometry(geometry_type)
[docs] def getThemeIcon(self, name: str) -> Any: """Get a theme icon via DialogEntityManager.""" return DialogEntityManager.get_theme_icon(name)
def _load_interpretations(self): """Load interpretations via interpretation_manager.""" self.interpretation_manager.load_interpretations() def _save_interpretations(self): """Save interpretations via interpretation_manager.""" self.interpretation_manager.save_interpretations() def _load_user_settings(self): """Load user settings via settings_manager.""" self.settings_manager.load_settings() def _save_user_settings(self): """Save user settings via settings_manager.""" self.settings_manager.save_settings()