Source code for sec_interp.gui.main_dialog_settings

from __future__ import annotations

"""Settings management module for SecInterp main dialog.

This module handles persistence of user settings between sessions.
"""

from typing import TYPE_CHECKING, Any

from sec_interp.logger_config import get_logger

from .main_dialog_config import DialogDefaults

if TYPE_CHECKING:
    pass

logger = get_logger(__name__)


[docs] class DialogSettingsManager: """Manages persistence of dialog settings."""
[docs] def __init__(self, dialog: sec_interp.gui.main_dialog.SecInterpDialog): """Initialize settings manager with reference to parent dialog. Args: dialog: The :class:`sec_interp.gui.main_dialog.SecInterpDialog` instance """ self.dialog = dialog # Access config service through the plugin instance controller # Safety check for tests where plugin_instance might be mock or None self.config = None if hasattr(self.dialog, "plugin_instance") and self.dialog.plugin_instance: self.config = self.dialog.plugin_instance.controller.config_service
[docs] def load_settings(self) -> None: """Load user settings from previous session.""" self._load_section_settings() self._load_dem_settings() self._load_geology_settings() self._load_structure_settings() self._load_drillhole_settings() self._load_output_settings() self._load_interpretation_settings() self._load_preview_settings() # Update all status indicators after bulk restoration self.dialog.status_manager.update_all() logger.info("Settings loaded successfully from Project/Global config")
def _load_section_settings(self) -> None: """Load settings for the Section page.""" p_sect = self.dialog.page_section self._restore_layer(p_sect.line_combo, "section_layer") buffer_dist = self._get_setting("buffer_dist") if buffer_dist is not None: p_sect.buffer_spin.setValue(float(buffer_dist)) def _load_dem_settings(self) -> None: """Load settings for the DEM page.""" p_dem = self.dialog.page_dem self._restore_layer(p_dem.raster_combo, "dem_layer") raster_layer = p_dem.raster_combo.currentLayer() if raster_layer: p_dem.band_combo.setLayer(raster_layer) band_idx = self._get_setting("dem_band") if band_idx is not None: p_dem.band_combo.setBand(int(band_idx)) scale = self._get_setting("scale") if scale is not None: p_dem.scale_spin.setValue(float(scale)) vert_exag = self._get_setting("vert_exag") if vert_exag is not None: p_dem.vertexag_spin.setValue(float(vert_exag)) if raster_layer: p_dem.scale_spin.blockSignals(True) p_dem._update_resolution() p_dem.scale_spin.blockSignals(False) if scale is not None: p_dem.scale_spin.setValue(float(scale)) def _load_geology_settings(self) -> None: """Load settings for the Geology page.""" p_geol = self.dialog.page_geology self._restore_layer(p_geol.layer_combo, "geol_layer") geol_layer = p_geol.layer_combo.currentLayer() if geol_layer: p_geol.field_combo.setLayer(geol_layer) self._restore_field(p_geol.field_combo, "geol_field") def _load_structure_settings(self) -> None: """Load settings for the Structure page.""" p_struct = self.dialog.page_struct self._restore_layer(p_struct.layer_combo, "struct_layer") struct_layer = p_struct.layer_combo.currentLayer() if struct_layer: p_struct.dip_combo.setLayer(struct_layer) p_struct.strike_combo.setLayer(struct_layer) self._restore_field(p_struct.dip_combo, "struct_dip_field") self._restore_field(p_struct.strike_combo, "struct_strike_field") dip_scale = self._get_setting("dip_scale_factor") if dip_scale is not None: p_struct.scale_spin.setValue(float(dip_scale)) def _load_drillhole_settings(self) -> None: """Load settings for the Drillhole page.""" dpage = self.dialog.page_drillhole self._restore_layer(dpage.c_layer, "dh_collar_layer") c_layer = dpage.c_layer.currentLayer() if c_layer: for w in [dpage.c_id, dpage.c_x, dpage.c_y, dpage.c_z, dpage.c_depth]: w.setLayer(c_layer) self._restore_field(dpage.c_id, "dh_collar_id") self._restore_check(dpage.chk_use_geom, "dh_use_geom") self._restore_field(dpage.c_x, "dh_collar_x") self._restore_field(dpage.c_y, "dh_collar_y") self._restore_field(dpage.c_z, "dh_collar_z") self._restore_field(dpage.c_depth, "dh_collar_depth") self._restore_layer(dpage.s_layer, "dh_survey_layer") s_layer = dpage.s_layer.currentLayer() if s_layer: for w in [dpage.s_id, dpage.s_depth, dpage.s_azim, dpage.s_incl]: w.setLayer(s_layer) self._restore_field(dpage.s_id, "dh_survey_id") self._restore_field(dpage.s_depth, "dh_survey_depth") self._restore_field(dpage.s_azim, "dh_survey_azim") self._restore_field(dpage.s_incl, "dh_survey_incl") self._restore_layer(dpage.i_layer, "dh_interval_layer") i_layer = dpage.i_layer.currentLayer() if i_layer: for w in [dpage.i_id, dpage.i_from, dpage.i_to, dpage.i_lith]: w.setLayer(i_layer) self._restore_field(dpage.i_id, "dh_interval_id") self._restore_field(dpage.i_from, "dh_interval_from") self._restore_field(dpage.i_to, "dh_interval_to") self._restore_field(dpage.i_lith, "dh_interval_lith") def _load_output_settings(self) -> None: """Load settings for output path.""" last_dir = self._get_setting("last_output_dir") if last_dir: self.dialog.output_widget.setFilePath(str(last_dir)) def _load_interpretation_settings(self) -> None: """Load settings for the Interpretation page.""" p_interp = self.dialog.page_interpretation self._restore_check(p_interp.chk_inherit_geol, "interp_inherit_geol") self._restore_check(p_interp.chk_inherit_drill, "interp_inherit_drill") custom_fields_json = self._get_setting("interp_custom_fields") if custom_fields_json: try: import json fields = json.loads(custom_fields_json) p_interp.fields_table.setRowCount(0) for f in fields: p_interp._add_field_row() row = p_interp.fields_table.rowCount() - 1 p_interp.fields_table.item(row, 0).setText(f.get("name", "")) p_interp.fields_table.cellWidget(row, 1).setCurrentText(f.get("type", "String")) p_interp.fields_table.item(row, 2).setText(f.get("default", "")) except Exception as e: logger.warning(f"Failed to restore custom fields: {e}") def _load_preview_settings(self) -> None: """Load settings for the preview widget.""" pw = self.dialog.preview_widget self._restore_check(pw.chk_topo, "show_topo") self._restore_check(pw.chk_geol, "show_geol") self._restore_check(pw.chk_struct, "show_struct") self._restore_check(pw.chk_drillholes, "show_drillholes") self._restore_check(pw.chk_interpretations, "show_interpretations") self._restore_check(pw.chk_legend, "show_legend") self._restore_check(pw.chk_auto_lod, "auto_lod") self._restore_check(pw.chk_adaptive_sampling, "adaptive_sampling") max_points = self._get_setting("max_points") if max_points is not None: pw.spin_max_points.setValue(int(max_points))
[docs] def save_settings(self) -> None: """Save user settings for next session.""" if not self.config: return self._save_section_settings() self._save_dem_settings() self._save_geology_settings() self._save_structure_settings() self._save_drillhole_settings() self._save_output_settings() self._save_interpretation_settings() self._save_preview_settings()
def _save_section_settings(self) -> None: """Save settings for the Section page.""" self._save_layer(self.dialog.page_section.line_combo, "section_layer") self._set_setting("buffer_dist", self.dialog.page_section.buffer_spin.value()) def _save_dem_settings(self) -> None: """Save settings for the DEM page.""" self._save_layer(self.dialog.page_dem.raster_combo, "dem_layer") self._set_setting("dem_band", self.dialog.page_dem.band_combo.currentBand()) self._set_setting("scale", self.dialog.page_dem.scale_spin.value()) self._set_setting("vert_exag", self.dialog.page_dem.vertexag_spin.value()) def _save_geology_settings(self) -> None: """Save settings for the Geology page.""" self._save_layer(self.dialog.page_geology.layer_combo, "geol_layer") self._save_field(self.dialog.page_geology.field_combo, "geol_field") def _save_structure_settings(self) -> None: """Save settings for the Structure page.""" self._save_layer(self.dialog.page_struct.layer_combo, "struct_layer") self._save_field(self.dialog.page_struct.dip_combo, "struct_dip_field") self._save_field(self.dialog.page_struct.strike_combo, "struct_strike_field") self._set_setting("dip_scale_factor", self.dialog.page_struct.scale_spin.value()) def _save_drillhole_settings(self) -> None: """Save settings for the Drillhole page.""" dpage = self.dialog.page_drillhole self._save_layer(dpage.c_layer, "dh_collar_layer") self._save_field(dpage.c_id, "dh_collar_id") self._save_check(dpage.chk_use_geom, "dh_use_geom") self._save_field(dpage.c_x, "dh_collar_x") self._save_field(dpage.c_y, "dh_collar_y") self._save_field(dpage.c_z, "dh_collar_z") self._save_field(dpage.c_depth, "dh_collar_depth") self._save_layer(dpage.s_layer, "dh_survey_layer") self._save_field(dpage.s_id, "dh_survey_id") self._save_field(dpage.s_depth, "dh_survey_depth") self._save_field(dpage.s_azim, "dh_survey_azim") self._save_field(dpage.s_incl, "dh_survey_incl") self._save_layer(dpage.i_layer, "dh_interval_layer") self._save_field(dpage.i_id, "dh_interval_id") self._save_field(dpage.i_from, "dh_interval_from") self._save_field(dpage.i_to, "dh_interval_to") self._save_field(dpage.i_lith, "dh_interval_lith") def _save_output_settings(self) -> None: """Save output path settings.""" self._set_setting("last_output_dir", self.dialog.output_widget.filePath()) def _save_interpretation_settings(self) -> None: """Save settings for the Interpretation page.""" p_interp = self.dialog.page_interpretation self._save_check(p_interp.chk_inherit_geol, "interp_inherit_geol") self._save_check(p_interp.chk_inherit_drill, "interp_inherit_drill") import json custom_fields = p_interp.get_data()["custom_fields"] self._set_setting("interp_custom_fields", json.dumps(custom_fields)) def _save_preview_settings(self) -> None: """Save settings for the preview widget.""" pw = self.dialog.preview_widget self._save_check(pw.chk_topo, "show_topo") self._save_check(pw.chk_geol, "show_geol") self._save_check(pw.chk_struct, "show_struct") self._save_check(pw.chk_drillholes, "show_drillholes") self._save_check(pw.chk_interpretations, "show_interpretations") self._save_check(pw.chk_legend, "show_legend") self._save_check(pw.chk_auto_lod, "auto_lod") self._save_check(pw.chk_adaptive_sampling, "adaptive_sampling") self._set_setting("max_points", pw.spin_max_points.value())
[docs] def reset_to_defaults(self) -> None: """Reset all dialog inputs to their default values.""" # --- Section Page --- self.dialog.page_section.line_combo.setLayer(None) self.dialog.page_section.buffer_spin.setValue(float(DialogDefaults.BUFFER_DISTANCE)) # --- DEM Page --- self.dialog.page_dem.raster_combo.setLayer(None) self.dialog.page_dem.band_combo.setBand(DialogDefaults.DEFAULT_BAND) self.dialog.page_dem.scale_spin.setValue(float(DialogDefaults.SCALE)) self.dialog.page_dem.vertexag_spin.setValue(float(DialogDefaults.VERTICAL_EXAGGERATION)) # --- Geology Page --- self.dialog.page_geology.layer_combo.setLayer(None) self.dialog.page_geology.field_combo.setField("") # --- Structure Page --- self.dialog.page_struct.layer_combo.setLayer(None) self.dialog.page_struct.dip_combo.setField("") self.dialog.page_struct.strike_combo.setField("") self.dialog.page_struct.scale_spin.setValue(float(DialogDefaults.DIP_SCALE_FACTOR)) # --- Drillhole Page --- dpage = self.dialog.page_drillhole dpage.c_layer.setLayer(None) dpage.c_id.setField("") dpage.chk_use_geom.setChecked(True) dpage.c_x.setField("") dpage.c_y.setField("") dpage.c_z.setField("") dpage.c_depth.setField("") dpage.s_layer.setLayer(None) dpage.s_id.setField("") dpage.s_depth.setField("") dpage.s_azim.setField("") dpage.s_incl.setField("") dpage.i_layer.setLayer(None) dpage.i_id.setField("") dpage.i_from.setField("") dpage.i_to.setField("") dpage.i_lith.setField("") # Output folder self.dialog.output_widget.setFilePath("") # --- Interpretation Page --- self.dialog.page_interpretation.fields_table.setRowCount(0) self.dialog.page_interpretation.chk_inherit_geol.setChecked(True) self.dialog.page_interpretation.chk_inherit_drill.setChecked(True) # --- Preview Widget --- pw = self.dialog.preview_widget pw.chk_topo.setChecked(True) pw.chk_geol.setChecked(True) pw.chk_struct.setChecked(True) pw.chk_drillholes.setChecked(True) pw.chk_interpretations.setChecked(True) pw.chk_legend.setChecked(True) pw.chk_auto_lod.setChecked(False) pw.chk_adaptive_sampling.setChecked(True) pw.spin_max_points.setValue(1000) # Clear interpretations and measurements if hasattr(self.dialog, "tool_manager"): self.dialog.tool_manager.measure_tool.reset() if hasattr(self.dialog, "interpretations"): self.dialog.interpretations = [] # Persist empty state to project if hasattr(self.dialog, "_save_interpretations"): self.dialog._save_interpretations() logger.info("Persistent interpretations cleared by reset") self.dialog.preview_widget.results_text.append( self.dialog.tr("✓ Form reset to default values") ) logger.info("Dialog reset to defaults by user")
# --- Helper Methods --- def _parse_setting_value(self, val: Any) -> Any: """Parse string values back to appropriate Python types.""" if val in (None, "", "None", "NULL"): return None val_str = str(val).lower() if val_str == "true": return True if val_str == "false": return False try: if "." in str(val): return float(val) return int(val) except ValueError: return val def _get_setting(self, key: str, default: Any = None) -> Any: """Get setting from Project (multiple scopes) first, then Global config.""" # 1. Try Project (New Scope) val, ok = self.dialog.project.readEntry("SecInterp", key, "") if not ok or val in (None, "", "None", "NULL"): # 1b. Try Project (Legacy Scope) val, ok = self.dialog.project.readEntry("SecInterpUI", key, "") if ok and val not in (None, "", "None", "NULL"): parsed = self._parse_setting_value(val) if parsed is not None: logger.debug(f"Project setting hit: {key} = {parsed}") return parsed # 2. Try Global fallback if self.config: val = self.config.get(key, default) parsed = self._parse_setting_value(val) if parsed is not None: logger.debug(f"Global setting fallback: {key} = {parsed}") return parsed return default def _set_setting(self, key: str, value: Any) -> None: """Set setting in both Project and Global config.""" if value is None: value = "" # 1. Save to Project self.dialog.project.writeEntry("SecInterp", key, str(value)) logger.debug(f"Setting saved to project: {key} = {value}") # 2. Save to Global if self.config: self.config.set(key, value) def _save_layer(self, combo, key: str) -> None: """Save selected layer ID and Name.""" layer = combo.currentLayer() if layer: self._set_setting(key, layer.id()) self._set_setting(f"{key}_name", layer.name()) else: self._set_setting(key, "") self._set_setting(f"{key}_name", "") def _restore_layer(self, combo, key: str) -> None: """Restore layer selection by ID or Name fallback.""" layer_id = self._get_setting(key) layer_name = self._get_setting(f"{key}_name") if not layer_id and not layer_name: return layer = None # 1. Try by ID (most accurate for current project) if layer_id: layer = self.dialog.project.mapLayer(str(layer_id)) # 2. Try by Name (fallback for cross-project or reloads) if not layer and layer_name: for lyr in self.dialog.project.mapLayers().values(): if lyr.name() == layer_name: layer = lyr break if layer: logger.debug(f"Restoring layer: {key} -> {layer.name()}") # Block signals to prevent cascade overwrites (e.g. scale suggestion) combo.blockSignals(True) combo.setLayer(layer) combo.blockSignals(False) else: logger.debug(f"Failed to restore layer for {key}") def _save_field(self, combo, key: str) -> None: """Save selected field name.""" self._set_setting(key, combo.currentField()) def _restore_field(self, combo, key: str) -> None: """Restore field selection.""" field = self._get_setting(key) if field: combo.setField(field) def _save_check(self, checkbox, key: str) -> None: """Save checkbox state.""" self._set_setting(key, checkbox.isChecked()) def _restore_check(self, checkbox, key: str) -> None: """Restore checkbox state.""" checked = self._get_setting(key) if checked is not None and checked != "": checkbox.setChecked(bool(checked))