""" Ventana principal de AR-ShipDesign. Layout inspirado en DELFTship: ┌─────────────────────────────────────────────────────────┐ │ Menú | Toolbar │ ├──────────┬──────────────────────────────┬───────────────┤ │ │ │ │ │ Árbol │ Vista central (tabs) │ Propiedades │ │ Proyecto│ 3D / Líneas / Análisis │ │ │ │ │ │ ├──────────┴──────────────────────────────┴───────────────┤ │ PANEL HIDROSTÁTICOS EN VIVO (siempre visible) │ ├─────────────────────────────────────────────────────────┤ │ Barra de tabs de módulos │ └─────────────────────────────────────────────────────────┘ """ from __future__ import annotations import json from pathlib import Path from typing import Optional from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtGui import QAction, QFont, QIcon, QKeySequence from PySide6.QtWidgets import ( QApplication, QDockWidget, QFileDialog, QFrame, QHBoxLayout, QLabel, QMainWindow, QMessageBox, QSizePolicy, QSplitter, QStatusBar, QTabWidget, QToolBar, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, ) from arshipdesign import __version__ from arshipdesign.core.project import Project from arshipdesign.utils.logger import get_logger from arshipdesign.utils.settings import ( add_recent_file, get_language, get_recent_files, get_settings, get_theme, set_theme, ) logger = get_logger("ui.main_window") # Carga de strings de i18n def _load_i18n(lang: str = "es") -> dict: i18n_path = Path(__file__).parent / "i18n" / f"{lang}.json" if not i18n_path.exists(): i18n_path = Path(__file__).parent / "i18n" / "es.json" try: return json.loads(i18n_path.read_text(encoding="utf-8")) except Exception: return {} class HydrostaticsPanel(QFrame): """ Panel de hidrostáticos en vivo — siempre visible en la parte inferior. En Sprint 2 se conectará al motor de cálculo. Por ahora muestra valores placeholder. """ def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.strings = strings self.setObjectName("hydrostaticsPanel") self.setFixedHeight(62) self._build_ui() def _build_ui(self) -> None: layout = QHBoxLayout(self) layout.setContentsMargins(10, 4, 10, 4) layout.setSpacing(0) # Título title = QLabel(" HIDROSTÁTICOS ") title.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold)) title.setProperty("label", True) layout.addWidget(title) sep = self._make_sep() layout.addWidget(sep) # Campos hidrostáticos self._fields: dict[str, QLabel] = {} hydro_items = [ ("T", "3.20 m", "Calado [m]"), ("Δ", "2 845 t", "Desplazamiento [t]"), ("LCB", "12.30 m", "Centro Long. Carena [m desde AP]"), ("KB", "1.85 m", "Centro Vert. Carena [m]"), ("KMT", "4.20 m", "Altura Metacéntrica Transv. [m]"), ("GMT", "1.05 m", "Altura Metacéntrica Corregida [m]"), ("TPC", "8.2 t/cm", "Toneladas por cm de Inmersión"), ("MCT", "42.5 t·m/cm", "Momento para Cambiar Asiento 1 cm"), ("Cb", "0.682", "Coeficiente de Bloque"), ("Cw", "0.821", "Coeficiente Plano Flotación"), ("Cm", "0.985", "Coeficiente Cuaderna Maestra"), ] for key, default_val, tooltip in hydro_items: lbl_key = QLabel(f" {key} ") lbl_key.setProperty("label", True) lbl_key.setToolTip(tooltip) lbl_val = QLabel(default_val) lbl_val.setProperty("value", True) lbl_val.setToolTip(tooltip) lbl_val.setMinimumWidth(72) self._fields[key] = lbl_val layout.addWidget(lbl_key) layout.addWidget(lbl_val) sep = self._make_sep() layout.addWidget(sep) # Indicador IMO self._imo_label = QLabel(" ⚠ IMO — ") self._imo_label.setProperty("label", True) self._imo_status = QLabel("SIN DATOS") self._imo_status.setToolTip("Cumplimiento IMO IS Code 2008. Activo cuando haya un caso de carga calculado.") layout.addWidget(self._imo_label) layout.addWidget(self._imo_status) layout.addStretch() @staticmethod def _make_sep() -> QFrame: sep = QFrame() sep.setFrameShape(QFrame.Shape.VLine) sep.setFrameShadow(QFrame.Shadow.Sunken) sep.setFixedWidth(1) sep.setStyleSheet("QFrame { color: #3a3f4b; margin: 6px 4px; }") return sep def update_values(self, values: dict[str, str]) -> None: """Actualiza los valores del panel. Llamar desde el motor de cálculo.""" for key, val in values.items(): if key in self._fields: self._fields[key].setText(val) def set_imo_status(self, ok: bool, detail: str = "") -> None: if ok: self._imo_status.setText("✅ CUMPLE") self._imo_status.setProperty("imo_ok", True) self._imo_status.setProperty("imo_fail", False) else: self._imo_status.setText(f"❌ FALLA {detail}") self._imo_status.setProperty("imo_ok", False) self._imo_status.setProperty("imo_fail", True) self._imo_status.style().polish(self._imo_status) class ProjectTreePanel(QWidget): """Panel árbol de proyecto (izquierda).""" item_selected = Signal(str) # nombre del ítem seleccionado def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.strings = strings self.setObjectName("projectTree") self.setMinimumWidth(180) self._build_ui() def _build_ui(self) -> None: layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) title = QLabel(f" {self.strings.get('panel_project', 'Proyecto')}") title.setFixedHeight(28) title.setStyleSheet("background: #252830; color: #90caf9; font-weight: bold; padding-left: 8px;") layout.addWidget(title) self.tree = QTreeWidget() self.tree.setHeaderHidden(True) self.tree.setIndentation(16) self.tree.setAnimated(True) layout.addWidget(self.tree) self._populate_default() self.tree.itemClicked.connect(self._on_item_clicked) def _populate_default(self) -> None: self.tree.clear() root_items = [ ("🚢 Buque", [ ("📐 Casco", ["Superficie 1"]), ("⚓ Apéndices", ["Quilla", "Timón"]), ("🏗 Superestructura", []), ]), ("⛽ Tanques", ["FO 1 BR", "FO 1 ER", "FW 1", "Lastre AP"]), ("📦 Bodegas", []), ("📊 Casos de Carga", ["Lightship", "Salida Lleno", "Llegada Lleno", "Lastre"]), ("⛵ Aparejo", ["Mástil Principal", "Mayor", "Génova"]), ("⚙ Motor", ["Motor Principal", "Hélice"]), ("🔌 Sistemas", [ "Eléctrico", "Combustible", "Agua Dulce", "Achique", "Lastre", "C. Incendios", "HVAC" ]), ("🏭 Fabricación CNC", []), ("🧴 Moldes FRP", []), ] for name, children in root_items: parent = QTreeWidgetItem(self.tree, [name]) parent.setExpanded(False) self._add_children(parent, children) self.tree.expandToDepth(0) def _add_children(self, parent: QTreeWidgetItem, children: list) -> None: for child in children: if isinstance(child, tuple): child_name, grandchildren = child child_item = QTreeWidgetItem(parent, [child_name]) self._add_children(child_item, grandchildren) else: QTreeWidgetItem(parent, [child]) def _on_item_clicked(self, item: QTreeWidgetItem, _col: int) -> None: self.item_selected.emit(item.text(0)) def set_project(self, project: Project) -> None: """Actualiza el árbol con los datos del proyecto. Sprint 1.""" pass # Se implementará en Sprint 1 class PropertiesPanel(QWidget): """Panel de propiedades del ítem seleccionado (derecha).""" def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.strings = strings self.setObjectName("propertiesPanel") self.setMinimumWidth(200) self._build_ui() def _build_ui(self) -> None: layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) title = QLabel(f" {self.strings.get('panel_properties', 'Propiedades')}") title.setFixedHeight(28) title.setStyleSheet("background: #252830; color: #90caf9; font-weight: bold; padding-left: 8px;") layout.addWidget(title) # Placeholder con dimensiones principales content = QWidget() content_layout = QVBoxLayout(content) content_layout.setContentsMargins(10, 10, 10, 10) content_layout.setSpacing(6) props = [ ("LOA", "— m"), ("LPP", "— m"), ("B", "— m"), ("T", "— m"), ("D", "— m"), ("Δ", "— t"), ("GMT", "— m"), ] mono_font = QFont("Consolas", 11) for label, value in props: row = QHBoxLayout() lbl = QLabel(label) lbl.setProperty("muted", True) lbl.setFixedWidth(50) val = QLabel(value) val.setFont(mono_font) row.addWidget(lbl) row.addWidget(val) row.addStretch() content_layout.addLayout(row) content_layout.addStretch() layout.addWidget(content) class CentralTabsWidget(QWidget): """ Widget central con tabs de vistas. En Sprint 0 muestra placeholders. Los viewers reales (3D, líneas, etc.) se implementan en Sprint 1. """ def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.strings = strings self._build_ui() def _build_ui(self) -> None: layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.tabs = QTabWidget() self.tabs.setTabPosition(QTabWidget.TabPosition.North) tab_names = [ ("tab_3d", "3D"), ("tab_lines", "Líneas"), ("tab_offsets", "Offsets"), ("tab_curves", "Curvas Hidrost."), ("tab_tanks", "Tanques"), ("tab_capacity", "Capacidad"), ("tab_stability", "Estabilidad GZ"), ("tab_resistance", "Resistencia"), ("tab_propulsion", "Propulsión"), ("tab_vpp", "VPP Velero"), ("tab_seakeeping", "Movimientos"), ("tab_electrical", "Eléctrico"), ("tab_fuel", "Combustible"), ("tab_freshwater", "Agua Dulce"), ("tab_bilge", "Achique"), ("tab_firefighting", "C. Incendios"), ("tab_hvac", "HVAC"), ("tab_scantling", "Escantillado"), ("tab_fabrication", "Fabricación CNC"), ("tab_molds", "Moldes FRP"), ("tab_report", "Reporte"), ] for key, default_name in tab_names: name = self.strings.get(key, default_name) placeholder = self._make_placeholder(name) self.tabs.addTab(placeholder, name) layout.addWidget(self.tabs) @staticmethod def _make_placeholder(tab_name: str) -> QWidget: w = QWidget() layout = QVBoxLayout(w) layout.setAlignment(Qt.AlignmentFlag.AlignCenter) icon_label = QLabel("🔜") icon_label.setFont(QFont("Segoe UI Emoji", 48)) icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) msg = QLabel(f"Módulo: {tab_name}\nSe implementará en el sprint correspondiente.") msg.setAlignment(Qt.AlignmentFlag.AlignCenter) msg.setProperty("muted", True) layout.addWidget(icon_label) layout.addWidget(msg) return w class NewProjectDialog(QMessageBox): """Dialog simple de nuevo proyecto para Sprint 0.""" pass class MainWindow(QMainWindow): """ Ventana principal de AR-ShipDesign. Implementa el layout DELFTship-style con: - Menú bar completo - Toolbar principal - Panel árbol de proyecto (izquierda, dock) - Vista central con tabs de módulos - Panel de propiedades (derecha, dock) - Panel de hidrostáticos en vivo (inferior, fijo) - Barra de estado """ def __init__(self) -> None: super().__init__() self._project: Optional[Project] = None self._lang = get_language() self._strings = _load_i18n(self._lang) self._setup_ui() self._setup_menu() self._setup_toolbar() self._setup_status_bar() self._restore_geometry() self._update_title() logger.info("MainWindow inicializada") # ────────────────────────────────────────────── # SETUP UI # ────────────────────────────────────────────── def _setup_ui(self) -> None: self.setMinimumSize(1100, 700) # Panel árbol proyecto (dock izquierda) self._project_tree = ProjectTreePanel(self._strings) dock_tree = QDockWidget(self._strings.get("panel_project", "Proyecto"), self) dock_tree.setObjectName("dockProjectTree") dock_tree.setWidget(self._project_tree) dock_tree.setFeatures( QDockWidget.DockWidgetFeature.DockWidgetMovable | QDockWidget.DockWidgetFeature.DockWidgetFloatable ) dock_tree.setMinimumWidth(200) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock_tree) # Panel propiedades (dock derecha) self._properties_panel = PropertiesPanel(self._strings) dock_props = QDockWidget(self._strings.get("panel_properties", "Propiedades"), self) dock_props.setObjectName("dockProperties") dock_props.setWidget(self._properties_panel) dock_props.setFeatures( QDockWidget.DockWidgetFeature.DockWidgetMovable | QDockWidget.DockWidgetFeature.DockWidgetFloatable ) dock_props.setMinimumWidth(220) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock_props) # Widget central con tabs self._central_tabs = CentralTabsWidget(self._strings) self.setCentralWidget(self._central_tabs) # Panel hidrostáticos (dock inferior, no ocultable) self._hydro_panel = HydrostaticsPanel(self._strings) dock_hydro = QDockWidget(self._strings.get("panel_hydrostatics", "Hidrostáticos"), self) dock_hydro.setObjectName("dockHydrostatics") dock_hydro.setWidget(self._hydro_panel) dock_hydro.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) dock_hydro.setTitleBarWidget(QWidget()) # Oculta la barra de título del dock self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, dock_hydro) # Conectar señales self._project_tree.item_selected.connect(self._on_tree_item_selected) def _setup_menu(self) -> None: s = self._strings menubar = self.menuBar() # ── ARCHIVO ── menu_file = menubar.addMenu(s.get("menu_file", "Archivo")) act_new = QAction(s.get("file_new", "Nuevo proyecto"), self) act_new.setShortcut(QKeySequence.StandardKey.New) act_new.triggered.connect(self._on_new_project) menu_file.addAction(act_new) act_open = QAction(s.get("file_open", "Abrir..."), self) act_open.setShortcut(QKeySequence.StandardKey.Open) act_open.triggered.connect(self._on_open_project) menu_file.addAction(act_open) menu_file.addSeparator() act_save = QAction(s.get("file_save", "Guardar"), self) act_save.setShortcut(QKeySequence.StandardKey.Save) act_save.triggered.connect(self._on_save_project) menu_file.addAction(act_save) act_save_as = QAction(s.get("file_save_as", "Guardar como..."), self) act_save_as.setShortcut(QKeySequence("Ctrl+Shift+S")) act_save_as.triggered.connect(self._on_save_as_project) menu_file.addAction(act_save_as) menu_file.addSeparator() # Recientes self._recent_menu = menu_file.addMenu(s.get("file_recent", "Recientes")) self._update_recent_menu() menu_file.addSeparator() act_exit = QAction(s.get("file_exit", "Salir"), self) act_exit.setShortcut(QKeySequence("Alt+F4")) act_exit.triggered.connect(self.close) menu_file.addAction(act_exit) # ── EDITAR ── menu_edit = menubar.addMenu(s.get("menu_edit", "Editar")) act_undo = QAction(s.get("edit_undo", "Deshacer"), self) act_undo.setShortcut(QKeySequence.StandardKey.Undo) act_undo.setEnabled(False) # Sprint 1 menu_edit.addAction(act_undo) act_redo = QAction(s.get("edit_redo", "Rehacer"), self) act_redo.setShortcut(QKeySequence.StandardKey.Redo) act_redo.setEnabled(False) # Sprint 1 menu_edit.addAction(act_redo) menu_edit.addSeparator() act_prefs = QAction(s.get("edit_preferences", "Preferencias..."), self) act_prefs.triggered.connect(self._on_preferences) menu_edit.addAction(act_prefs) # ── VER ── menu_view = menubar.addMenu(s.get("menu_view", "Ver")) act_theme = QAction("Cambiar tema (claro/oscuro)", self) act_theme.triggered.connect(self._on_toggle_theme) menu_view.addAction(act_theme) # ── MODELO ── menu_model = menubar.addMenu(s.get("menu_model", "Modelo")) menu_model.addAction("Nuevo casco... (Sprint 1)") menu_model.addAction("Wizard de embarcación... (Sprint 1)") menu_model.addAction("Importar offsets... (Sprint 1)") menu_model.addAction("Importar DXF... (Sprint 1)") # ── ANÁLISIS ── menu_analysis = menubar.addMenu(s.get("menu_analysis", "Análisis")) menu_analysis.addAction("Hidrostáticos (Sprint 2)") menu_analysis.addAction("Estabilidad GZ (Sprint 3)") menu_analysis.addAction("Escantillado ISO 12215 (Sprint 2.5)") menu_analysis.addAction("Resistencia y Propulsión (Sprint 5)") menu_analysis.addAction("VPP Velero (Sprint 6)") menu_analysis.addAction("Movimientos / Seakeeping (Sprint 9)") # ── SISTEMAS ── menu_systems = menubar.addMenu(s.get("menu_systems", "Sistemas")) for sys_name in ["Eléctrico (Sprint 7)", "Combustible (Sprint 7)", "Agua Dulce (Sprint 7)", "Achique (Sprint 7)", "Lastre (Sprint 7)", "C. Incendios (Sprint 8)", "HVAC (Sprint 8)", "Gobierno (Sprint 8)"]: menu_systems.addAction(sys_name) # ── FABRICACIÓN ── menu_fab = menubar.addMenu(s.get("menu_fabrication", "Fabricación")) menu_fab.addAction("Estimación de material (Sprint 13)") menu_fab.addAction("Nesting / Optimización de cortes (Sprint 13)") menu_fab.addAction("Generar G-code CNC (Sprint 13)") menu_fab.addSeparator() menu_fab.addAction("Moldes FRP — Lofting (Sprint 13B)") menu_fab.addAction("Moldes FRP — Schedule laminado (Sprint 13B)") menu_fab.addAction("Moldes FRP — BOM materiales (Sprint 13B)") # ── REPORTES ── menu_reports = menubar.addMenu(s.get("menu_reports", "Reportes")) menu_reports.addAction("Reporte Hidrostático (Sprint 10)") menu_reports.addAction("Cuaderno de Estabilidad (Sprint 10)") menu_reports.addAction("Plano de Líneas (Sprint 10)") menu_reports.addAction("Reporte Escantillado (Sprint 10)") menu_reports.addAction("Balance Eléctrico (Sprint 10)") # ── AYUDA ── menu_help = menubar.addMenu(s.get("menu_help", "Ayuda")) act_about = QAction(s.get("about_title", "Acerca de..."), self) act_about.triggered.connect(self._on_about) menu_help.addAction(act_about) def _setup_toolbar(self) -> None: tb = QToolBar("Principal", self) tb.setObjectName("mainToolbar") tb.setMovable(False) self.addToolBar(tb) buttons = [ ("🗎", "Nuevo proyecto (Ctrl+N)", self._on_new_project), ("📂", "Abrir proyecto (Ctrl+O)", self._on_open_project), ("💾", "Guardar (Ctrl+S)", self._on_save_project), ] for icon_text, tip, slot in buttons: btn = tb.addAction(icon_text) btn.setToolTip(tip) btn.triggered.connect(slot) tb.addSeparator() btn_undo = tb.addAction("↩") btn_undo.setToolTip("Deshacer (Ctrl+Z)") btn_undo.setEnabled(False) btn_redo = tb.addAction("↪") btn_redo.setToolTip("Rehacer (Ctrl+Y)") btn_redo.setEnabled(False) tb.addSeparator() btn_wizard = tb.addAction("🚢 Wizard") btn_wizard.setToolTip("Wizard de nueva embarcación (Sprint 1)") btn_wizard.setEnabled(False) tb.addSeparator() btn_theme = tb.addAction("☀/🌙") btn_theme.setToolTip("Cambiar tema claro/oscuro") btn_theme.triggered.connect(self._on_toggle_theme) # Separador flexible spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) tb.addWidget(spacer) # Selector de unidades tb.addWidget(QLabel(" Unidades: ")) self._units_label = QLabel("SI") self._units_label.setStyleSheet("color: #90caf9; font-weight: bold; margin-right: 8px;") tb.addWidget(self._units_label) # Idioma tb.addWidget(QLabel(" 🌍 ")) self._lang_label = QLabel(self._lang.upper()) self._lang_label.setStyleSheet("color: #90caf9; font-weight: bold; margin-right: 8px;") tb.addWidget(self._lang_label) def _setup_status_bar(self) -> None: sb = self.statusBar() sb.showMessage(self._strings.get("status_ready", "Listo")) self._status_version = QLabel(f" AR-ShipDesign v{__version__} ") sb.addPermanentWidget(self._status_version) # ────────────────────────────────────────────── # ACCIONES DE PROYECTO # ────────────────────────────────────────────── def _on_new_project(self) -> None: if self._project and self._project.is_modified: if not self._ask_save_changes(): return self._project = Project.new("Proyecto sin título") self._on_project_loaded() self.statusBar().showMessage("Nuevo proyecto creado") def _on_open_project(self) -> None: if self._project and self._project.is_modified: if not self._ask_save_changes(): return path, _ = QFileDialog.getOpenFileName( self, "Abrir proyecto AR-ShipDesign", str(Path.home()), "Proyectos AR-ShipDesign (*.arsd);;Todos los archivos (*)", ) if not path: return try: self._project = Project.load(Path(path)) add_recent_file(path) self._update_recent_menu() self._on_project_loaded() self.statusBar().showMessage(f"Proyecto abierto: {path}") except Exception as e: QMessageBox.critical(self, "Error al abrir", f"No se pudo abrir el proyecto:\n{e}") logger.error("Error abriendo proyecto: %s", e) def _on_save_project(self) -> None: if self._project is None: return if self._project.path is None: self._on_save_as_project() return try: self._project.save() self._update_title() self.statusBar().showMessage(f"Guardado: {self._project.path}") except Exception as e: QMessageBox.critical(self, "Error al guardar", str(e)) def _on_save_as_project(self) -> None: if self._project is None: return path, _ = QFileDialog.getSaveFileName( self, "Guardar proyecto como...", str(Path.home() / f"{self._project.name}.arsd"), "Proyectos AR-ShipDesign (*.arsd)", ) if not path: return try: self._project.save(Path(path)) add_recent_file(path) self._update_recent_menu() self._update_title() self.statusBar().showMessage(f"Guardado como: {path}") except Exception as e: QMessageBox.critical(self, "Error al guardar", str(e)) def _on_project_loaded(self) -> None: """Callback cuando se carga o crea un proyecto.""" self._update_title() self._project_tree.set_project(self._project) def _ask_save_changes(self) -> bool: """Pregunta si guardar antes de cerrar/nuevo. Retorna True si se puede continuar.""" reply = QMessageBox.question( self, "Cambios sin guardar", f"El proyecto '{self._project.name}' tiene cambios sin guardar.\n¿Desea guardar antes de continuar?", QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, ) if reply == QMessageBox.StandardButton.Save: self._on_save_project() return True elif reply == QMessageBox.StandardButton.Discard: return True return False # Cancel # ────────────────────────────────────────────── # ACCIONES DE UI # ────────────────────────────────────────────── def _on_tree_item_selected(self, name: str) -> None: self.statusBar().showMessage(f"Seleccionado: {name}") def _on_preferences(self) -> None: QMessageBox.information(self, "Preferencias", "Diálogo de preferencias — Sprint 1") def _on_toggle_theme(self) -> None: current = get_theme() new_theme = "light" if current == "dark" else "dark" set_theme(new_theme) self._apply_theme(new_theme) def _apply_theme(self, theme: str) -> None: qss_path = Path(__file__).parent / "themes" / f"{theme}.qss" try: qss = qss_path.read_text(encoding="utf-8") QApplication.instance().setStyleSheet(qss) except Exception as e: logger.warning("No se pudo aplicar el tema %s: %s", theme, e) def _on_about(self) -> None: QMessageBox.about( self, self._strings.get("about_title", "Acerca de AR-ShipDesign"), f"""AR-ShipDesign v{__version__}
Software profesional de diseño naval.

Motor geométrico: NURBS (geomdl)
Visualización 3D: PyVista + VTK
Estándares: ISO 12215, IMO IS Code 2008

{self._strings.get("about_copyright", "Copyright © 2025 Álvaro Rodríguez")}""", ) def _update_title(self) -> None: if self._project: self.setWindowTitle(f"AR-ShipDesign — {self._project.display_name}") else: self.setWindowTitle("AR-ShipDesign") def _update_recent_menu(self) -> None: self._recent_menu.clear() recent = get_recent_files() if not recent: act = QAction("(sin archivos recientes)", self) act.setEnabled(False) self._recent_menu.addAction(act) return for path in recent: act = QAction(Path(path).name, self) act.setToolTip(path) act.triggered.connect(lambda checked=False, p=path: self._open_recent(p)) self._recent_menu.addAction(act) def _open_recent(self, path: str) -> None: if self._project and self._project.is_modified: if not self._ask_save_changes(): return try: self._project = Project.load(Path(path)) add_recent_file(path) self._on_project_loaded() except Exception as e: QMessageBox.critical(self, "Error", f"No se pudo abrir:\n{e}") # ────────────────────────────────────────────── # GEOMETRÍA Y ESTADO # ────────────────────────────────────────────── def _restore_geometry(self) -> None: s = get_settings() geom = s.value("ui/windowGeometry") state = s.value("ui/windowState") if geom: self.restoreGeometry(geom) else: self.resize(1280, 800) self.showMaximized() if state: self.restoreState(state) def closeEvent(self, event) -> None: if self._project and self._project.is_modified: if not self._ask_save_changes(): event.ignore() return s = get_settings() s.setValue("ui/windowGeometry", self.saveGeometry()) s.setValue("ui/windowState", self.saveState()) event.accept()