diff --git a/arshipdesign/ui/main_window.py b/arshipdesign/ui/main_window.py
index b063476..b0ffab3 100644
--- a/arshipdesign/ui/main_window.py
+++ b/arshipdesign/ui/main_window.py
@@ -1,19 +1,18 @@
"""
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 │
- └─────────────────────────────────────────────────────────┘
+Layout estilo DELFTship / Maxsurf:
+ - Ribbon bar con pestañas funcionales
+ (Home / Geometría / Análisis / Tanques / Sistemas / Fabricación)
+ - Dock izquierdo: panel de capas (estilo DELFTship)
+ - Área central: 4 viewports por defecto
+ (Perspectiva 3D · Profile · Body Plan · Vista de Planta)
+ - Dock derecho: propiedades del elemento seleccionado
+ - Panel inferior fijo: hidrostáticos en vivo (36 px)
+ - Barra de estado
+
+Los módulos de análisis se activan desde el menú o el ribbon y
+reemplazan el área central temporalmente.
"""
from __future__ import annotations
@@ -22,8 +21,8 @@ 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.QtCore import Qt, QSize, Signal
+from PySide6.QtGui import QAction, QFont, QKeySequence, QIcon
from PySide6.QtWidgets import (
QApplication,
QDockWidget,
@@ -33,15 +32,16 @@ from PySide6.QtWidgets import (
QLabel,
QMainWindow,
QMessageBox,
+ QScrollArea,
QSizePolicy,
QSplitter,
- QStatusBar,
- QTabWidget,
+ QStackedWidget,
QToolBar,
- QTreeWidget,
- QTreeWidgetItem,
+ QToolButton,
QVBoxLayout,
QWidget,
+ QStyle,
+ QAbstractItemView,
)
from arshipdesign import __version__
@@ -58,125 +58,136 @@ from arshipdesign.utils.settings import (
logger = get_logger("ui.main_window")
-# Carga de strings de i18n
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Utilidades
+# ─────────────────────────────────────────────────────────────────────────────
+
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"
+ path = Path(__file__).parent / "i18n" / f"{lang}.json"
+ if not path.exists():
+ path = Path(__file__).parent / "i18n" / "es.json"
try:
- return json.loads(i18n_path.read_text(encoding="utf-8"))
+ return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
+def _spi(sp) -> QIcon:
+ """Icono estándar del sistema."""
+ return QApplication.style().standardIcon(sp)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# PANEL DE HIDROSTÁTICOS
+# ─────────────────────────────────────────────────────────────────────────────
+
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.
+ Barra de hidrostáticos en vivo — siempre visible en la parte inferior.
+ Se conecta al motor de cálculo en Sprint 2.
"""
def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
self.strings = strings
self.setObjectName("hydrostaticsPanel")
- self.setFixedHeight(62)
+ self.setFixedHeight(36)
self._build_ui()
def _build_ui(self) -> None:
layout = QHBoxLayout(self)
- layout.setContentsMargins(10, 4, 10, 4)
+ layout.setContentsMargins(10, 0, 10, 0)
layout.setSpacing(0)
- # Título
- title = QLabel(" HIDROSTÁTICOS ")
- title.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold))
- title.setProperty("label", True)
+ title = QLabel("HIDROSTÁTICOS")
+ title.setObjectName("hydroTitle")
layout.addWidget(title)
+ layout.addWidget(self._sep())
- 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"),
+ mono = QFont("Consolas", 11)
+
+ items = [
+ ("T", "—", "Calado [m]"),
+ ("Δ", "—", "Desplazamiento [t]"),
+ ("LCB", "—", "Centro Longitudinal de Carena [m desde AP]"),
+ ("KB", "—", "Centro Vertical de Carena [m]"),
+ ("KMT", "—", "Altura Metacéntrica Transversal [m]"),
+ ("GMT", "—", "Altura Metacéntrica Corregida [m]"),
+ ("TPC", "—", "Toneladas por cm de Inmersión"),
+ ("MCT", "—", "Momento para Cambiar Asiento 1 cm [t·m/cm]"),
+ ("Cb", "—", "Coeficiente de Bloque"),
+ ("Cw", "—", "Coeficiente de Plano de Flotación"),
+ ("Cm", "—", "Coeficiente de Cuaderna Maestra"),
]
- for key, default_val, tooltip in hydro_items:
- lbl_key = QLabel(f" {key} ")
- lbl_key.setProperty("label", True)
- lbl_key.setToolTip(tooltip)
+ for key, default, tip in items:
+ k = QLabel(f" {key} ")
+ k.setObjectName("hydroKey")
+ k.setToolTip(tip)
+ v = QLabel(default)
+ v.setObjectName("hydroVal")
+ v.setFont(mono)
+ v.setToolTip(tip)
+ v.setMinimumWidth(52)
+ self._fields[key] = v
+ layout.addWidget(k)
+ layout.addWidget(v)
+ layout.addWidget(self._sep())
- 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)
+ imo_k = QLabel(" IMO ")
+ imo_k.setObjectName("hydroKey")
+ self._imo = QLabel("SIN DATOS")
+ self._imo.setObjectName("hydroImoNone")
+ self._imo.setFont(mono)
+ self._imo.setToolTip("Cumplimiento IMO IS Code 2008")
+ layout.addWidget(imo_k)
+ layout.addWidget(self._imo)
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 _sep() -> QFrame:
+ f = QFrame()
+ f.setFrameShape(QFrame.Shape.VLine)
+ f.setObjectName("hydroSep")
+ f.setFixedWidth(1)
+ return f
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)
+ self._imo.setText("CUMPLE")
+ self._imo.setObjectName("hydroImoOk")
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)
+ self._imo.setText(f"FALLA {detail}".strip())
+ self._imo.setObjectName("hydroImoFail")
+ self._imo.style().polish(self._imo)
-class ProjectTreePanel(QWidget):
- """Panel árbol de proyecto (izquierda)."""
+# ─────────────────────────────────────────────────────────────────────────────
+# VIEWPORTS — 4 vistas estilo Maxsurf / DELFTship
+# ─────────────────────────────────────────────────────────────────────────────
- item_selected = Signal(str) # nombre del ítem seleccionado
+_VIEW_LABELS: dict[str, str] = {
+ "perspective": "Perspectiva 3D",
+ "profile": "Vista Lateral — Perfil",
+ "bodyplan": "Body Plan — Secciones",
+ "plan": "Vista de Planta",
+}
- def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None:
+
+class ViewportFrame(QFrame):
+ """Un viewport individual con barra de título."""
+
+ def __init__(self, view_type: str, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
- self.strings = strings
- self.setObjectName("projectTree")
- self.setMinimumWidth(180)
+ self.view_type = view_type
+ self.setObjectName("viewportFrame")
self._build_ui()
def _build_ui(self) -> None:
@@ -184,67 +195,255 @@ class ProjectTreePanel(QWidget):
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)
+ # ── Barra de título ──────────────────────────────────────
+ title_bar = QWidget()
+ title_bar.setObjectName("viewportTitleBar")
+ title_bar.setFixedHeight(22)
+ tbl = QHBoxLayout(title_bar)
+ tbl.setContentsMargins(8, 0, 4, 0)
+ tbl.setSpacing(0)
- self.tree = QTreeWidget()
- self.tree.setHeaderHidden(True)
- self.tree.setIndentation(16)
- self.tree.setAnimated(True)
- layout.addWidget(self.tree)
+ lbl = QLabel(_VIEW_LABELS.get(self.view_type, self.view_type))
+ lbl.setObjectName("viewportTitle")
+ tbl.addWidget(lbl)
+ tbl.addStretch()
+ layout.addWidget(title_bar)
- self._populate_default()
- self.tree.itemClicked.connect(self._on_item_clicked)
+ # ── Área de dibujo (placeholder Sprint 0) ────────────────
+ self._canvas = QWidget()
+ self._canvas.setObjectName("viewportCanvas")
+ cl = QVBoxLayout(self._canvas)
+ cl.setAlignment(Qt.AlignmentFlag.AlignCenter)
- 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", []),
- ]
+ ph = QLabel(_VIEW_LABELS.get(self.view_type, ""))
+ ph.setObjectName("viewportPlaceholder")
+ ph.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ cl.addWidget(ph)
+ layout.addWidget(self._canvas, 1)
- for name, children in root_items:
- parent = QTreeWidgetItem(self.tree, [name])
- parent.setExpanded(False)
- self._add_children(parent, children)
+ def set_canvas(self, widget: QWidget) -> None:
+ """Sprint 1: sustituye el placeholder por el widget 3D / 2D real."""
+ lo = self.layout()
+ if lo.count() > 1:
+ old = lo.takeAt(1).widget()
+ if old:
+ old.deleteLater()
+ lo.addWidget(widget, 1)
+ self._canvas = widget
- 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])
+class FourViewport(QWidget):
+ """
+ Layout de 4 viewports con separadores arrastrables.
+ Topología:
+ ┌─────────────┬──────────────┐
+ │ Perspectiva │ Perf. Lateral│
+ ├─────────────┼──────────────┤
+ │ Body Plan │ Vista Planta│
+ └─────────────┴──────────────┘
+ """
- def _on_item_clicked(self, item: QTreeWidgetItem, _col: int) -> None:
- self.item_selected.emit(item.text(0))
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
+ super().__init__(parent)
+ self.setObjectName("fourViewport")
+ self._build_ui()
+
+ def _build_ui(self) -> None:
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+
+ v_split = QSplitter(Qt.Orientation.Vertical)
+ v_split.setObjectName("viewportSplitter")
+ v_split.setHandleWidth(3)
+
+ top_split = QSplitter(Qt.Orientation.Horizontal)
+ top_split.setObjectName("viewportSplitter")
+ top_split.setHandleWidth(3)
+ self._vp_perspective = ViewportFrame("perspective")
+ self._vp_profile = ViewportFrame("profile")
+ top_split.addWidget(self._vp_perspective)
+ top_split.addWidget(self._vp_profile)
+ top_split.setSizes([600, 600])
+
+ bot_split = QSplitter(Qt.Orientation.Horizontal)
+ bot_split.setObjectName("viewportSplitter")
+ bot_split.setHandleWidth(3)
+ self._vp_bodyplan = ViewportFrame("bodyplan")
+ self._vp_plan = ViewportFrame("plan")
+ bot_split.addWidget(self._vp_bodyplan)
+ bot_split.addWidget(self._vp_plan)
+ bot_split.setSizes([600, 600])
+
+ v_split.addWidget(top_split)
+ v_split.addWidget(bot_split)
+ v_split.setSizes([400, 400])
+ layout.addWidget(v_split)
+
+ def viewport(self, view_type: str) -> Optional[ViewportFrame]:
+ return {
+ "perspective": self._vp_perspective,
+ "profile": self._vp_profile,
+ "bodyplan": self._vp_bodyplan,
+ "plan": self._vp_plan,
+ }.get(view_type)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# PANEL DE CAPAS (estilo DELFTship)
+# ─────────────────────────────────────────────────────────────────────────────
+
+_LAYER_PRESETS: list[tuple[str, str, bool, bool]] = [
+ # (nombre, color, visible, bloqueado)
+ ("Casco", "#4a9eff", True, False),
+ ("Superestructura", "#6ee7b7", True, False),
+ ("Apéndices", "#fbbf24", True, False),
+ ("Cubierta", "#f87171", True, False),
+ ("Interiores", "#a78bfa", False, False),
+ ("Tanques", "#34d399", True, False),
+ ("Estructuras", "#fb923c", False, False),
+ ("Líneas de ref.", "#94a3b8", True, False),
+ ("Moldes FRP", "#e879f9", False, False),
+ ("CNC Corte", "#e2e8f0", False, False),
+]
+
+
+class LayerRow(QWidget):
+ """Fila de una capa: ojo de visibilidad · candado · muestra de color · nombre."""
+
+ visibility_toggled = Signal(str, bool)
+ lock_toggled = Signal(str, bool)
+
+ def __init__(
+ self,
+ name: str,
+ color: str = "#4a9eff",
+ visible: bool = True,
+ locked: bool = False,
+ parent: Optional[QWidget] = None,
+ ) -> None:
+ super().__init__(parent)
+ self.layer_name = name
+ self.setObjectName("layerRow")
+ self._build_ui(name, color, visible, locked)
+
+ def _build_ui(self, name: str, color: str, visible: bool, locked: bool) -> None:
+ layout = QHBoxLayout(self)
+ layout.setContentsMargins(4, 1, 4, 1)
+ layout.setSpacing(3)
+
+ self._vis_btn = QToolButton()
+ self._vis_btn.setObjectName("layerVisBtn")
+ self._vis_btn.setCheckable(True)
+ self._vis_btn.setChecked(visible)
+ self._vis_btn.setFixedSize(16, 16)
+ self._vis_btn.setToolTip("Mostrar / Ocultar capa")
+ self._vis_btn.clicked.connect(lambda v: self.visibility_toggled.emit(name, v))
+
+ self._lock_btn = QToolButton()
+ self._lock_btn.setObjectName("layerLockBtn")
+ self._lock_btn.setCheckable(True)
+ self._lock_btn.setChecked(locked)
+ self._lock_btn.setFixedSize(16, 16)
+ self._lock_btn.setToolTip("Bloquear / Desbloquear capa")
+ self._lock_btn.clicked.connect(lambda v: self.lock_toggled.emit(name, v))
+
+ swatch = QLabel()
+ swatch.setObjectName("layerColorSwatch")
+ swatch.setFixedSize(12, 12)
+ swatch.setStyleSheet(
+ f"background-color:{color};border:1px solid #3a3f4b;border-radius:2px;"
+ )
+
+ lbl = QLabel(name)
+ lbl.setObjectName("layerName")
+
+ layout.addWidget(self._vis_btn)
+ layout.addWidget(self._lock_btn)
+ layout.addWidget(swatch)
+ layout.addWidget(lbl)
+ layout.addStretch()
+
+
+class LayersPanel(QWidget):
+ """Panel de capas — dock izquierdo (estilo DELFTship)."""
+
+ layer_selected = Signal(str)
+
+ def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None:
+ super().__init__(parent)
+ self.strings = strings
+ self.setObjectName("layersPanel")
+ self.setMinimumWidth(175)
+ self._build_ui()
+
+ def _build_ui(self) -> None:
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+
+ # ── Cabecera ─────────────────────────────────────────────
+ header = QWidget()
+ header.setObjectName("layersPanelHeader")
+ header.setFixedHeight(28)
+ hl = QHBoxLayout(header)
+ hl.setContentsMargins(4, 2, 4, 2)
+ hl.setSpacing(2)
+
+ sp = QStyle.StandardPixmap
+ for icon_sp, tip in (
+ (sp.SP_FileIcon, "Nueva capa"),
+ (sp.SP_DialogSaveButton, "Duplicar capa"),
+ ):
+ btn = QToolButton()
+ btn.setIcon(_spi(icon_sp))
+ btn.setFixedSize(22, 22)
+ btn.setToolTip(tip)
+ btn.setEnabled(False)
+ hl.addWidget(btn)
+ hl.addStretch()
+ layout.addWidget(header)
+
+ sep = QFrame()
+ sep.setFrameShape(QFrame.Shape.HLine)
+ sep.setObjectName("panelSep")
+ layout.addWidget(sep)
+
+ # ── Lista de capas ────────────────────────────────────────
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+ scroll.setObjectName("layersScrollArea")
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
+
+ container = QWidget()
+ container.setObjectName("layersContainer")
+ self._layer_layout = QVBoxLayout(container)
+ self._layer_layout.setContentsMargins(0, 0, 0, 0)
+ self._layer_layout.setSpacing(0)
+
+ for name, color, visible, locked in _LAYER_PRESETS:
+ row = LayerRow(name, color, visible, locked)
+ row.visibility_toggled.connect(
+ lambda n, v: logger.debug("Capa '%s' visibilidad=%s", n, v)
+ )
+ self._layer_layout.addWidget(row)
+
+ self._layer_layout.addStretch()
+ scroll.setWidget(container)
+ layout.addWidget(scroll, 1)
def set_project(self, project: Project) -> None:
- """Actualiza el árbol con los datos del proyecto. Sprint 1."""
- pass # Se implementará en Sprint 1
+ """Sprint 1: recargar capas desde los datos reales del proyecto."""
+ pass
+# ─────────────────────────────────────────────────────────────────────────────
+# PANEL DE PROPIEDADES
+# ─────────────────────────────────────────────────────────────────────────────
+
class PropertiesPanel(QWidget):
- """Panel de propiedades del ítem seleccionado (derecha)."""
+ """Propiedades del elemento seleccionado — dock derecho."""
def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
@@ -256,133 +455,338 @@ class PropertiesPanel(QWidget):
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)
+ content.setObjectName("propContent")
+ cl = QVBoxLayout(content)
+ cl.setContentsMargins(10, 10, 10, 10)
+ cl.setSpacing(4)
- props = [
- ("LOA", "— m"),
- ("LPP", "— m"),
- ("B", "— m"),
- ("T", "— m"),
- ("D", "— m"),
- ("Δ", "— t"),
- ("GMT", "— m"),
- ]
+ sec = QLabel("Dimensiones principales")
+ sec.setObjectName("propSection")
+ cl.addWidget(sec)
- mono_font = QFont("Consolas", 11)
- for label, value in props:
+ mono = QFont("Consolas", 11)
+ for lbl_text, tip in [
+ ("LOA", "Eslora total [m]"),
+ ("LPP", "Eslora entre perpendiculares [m]"),
+ ("B", "Manga [m]"),
+ ("T", "Calado [m]"),
+ ("D", "Puntal [m]"),
+ ("Δ", "Desplazamiento [t]"),
+ ("GMT", "Altura metacéntrica [m]"),
+ ]:
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.setSpacing(8)
+ k = QLabel(lbl_text)
+ k.setObjectName("propKey")
+ k.setFixedWidth(40)
+ k.setToolTip(tip)
+ v = QLabel("—")
+ v.setObjectName("propVal")
+ v.setFont(mono)
+ v.setToolTip(tip)
+ row.addWidget(k)
+ row.addWidget(v)
row.addStretch()
- content_layout.addLayout(row)
+ cl.addLayout(row)
- content_layout.addStretch()
+ cl.addStretch()
layout.addWidget(content)
-class CentralTabsWidget(QWidget):
- """
- Widget central con tabs de vistas.
+# ─────────────────────────────────────────────────────────────────────────────
+# ÁREA CENTRAL — módulo activo
+# ─────────────────────────────────────────────────────────────────────────────
- En Sprint 0 muestra placeholders.
- Los viewers reales (3D, líneas, etc.) se implementan en Sprint 1.
+class ModuleArea(QStackedWidget):
+ """
+ Área central con un módulo visible a la vez.
+ Índice 0 → FourViewport (vista de modelado estándar)
+ Índices 1..N → módulos de análisis / fabricación en pantalla completa
"""
- def __init__(self, strings: dict, parent: Optional[QWidget] = None) -> None:
+ # Índices de módulos
+ MOD_4VP = 0 # 4 viewports — vista de modelado (por defecto)
+ MOD_LINES = 1
+ MOD_OFFSETS = 2
+ MOD_CURVES = 3
+ MOD_TANKS = 4
+ MOD_CAPACITY = 5
+ MOD_STABILITY = 6
+ MOD_RESISTANCE = 7
+ MOD_PROPULSION = 8
+ MOD_VPP = 9
+ MOD_SEAKEEPING = 10
+ MOD_ELECTRICAL = 11
+ MOD_FUEL = 12
+ MOD_FRESHWATER = 13
+ MOD_BILGE = 14
+ MOD_FIREFIGHT = 15
+ MOD_HVAC = 16
+ MOD_SCANTLING = 17
+ MOD_CNC = 18
+ MOD_MOLDS = 19
+ MOD_REPORT = 20
+ MOD_ROUTING_PIPES = 21
+ MOD_ROUTING_CABLES = 22
+
+ # (nombre, descripción) — None en índice 0 (FourViewport manejado aparte)
+ _MODULE_INFO: list[Optional[tuple[str, str]]] = [
+ None,
+ ("Plano de Líneas",
+ "Visualización del plano de líneas tradicional (body plan, profile, plan)."),
+ ("Tabla de Offsets",
+ "Editor de offsets de secciones transversales."),
+ ("Curvas Hidrostáticas",
+ "Gráficas de hidrostáticos en función del calado."),
+ ("Modelado de Tanques",
+ "Definición geométrica de tanques y sentinas NURBS."),
+ ("Plan de Capacidad",
+ "Tablas de sondeo y plan de capacidad de tanques."),
+ ("Estabilidad GZ",
+ "Curva de brazos adrizantes GZ · Criterios IMO IS Code 2008."),
+ ("Resistencia al Avance",
+ "Métodos: Holtrop & Mennen · Savitsky · DSYHS · Hollenbach."),
+ ("Propulsión",
+ "Hélice de paso variable / fijo — Wageningen B-series."),
+ ("VPP Velero",
+ "Programa Velocidad-Potencia polar para veleros (DSYHS / ORC)."),
+ ("Seakeeping — Movimientos",
+ "Strip Theory (Salvesen-Tuck-Faltinsen). Espectros de respuesta."),
+ ("Balance Eléctrico (EPLA)",
+ "Lista de consumidores eléctricos y cálculo de generación."),
+ ("Sistema de Combustible",
+ "Diagrama de tuberías de combustible y cálculo de consumo."),
+ ("Sistema de Agua Dulce",
+ "Balance hídrico, tanques de agua dulce, producción."),
+ ("Sistema de Achique",
+ "Cálculo de bombas, diámetros de tuberías, caudal."),
+ ("Sistema Contra Incendios",
+ "Detección, supresión y cumplimiento SOLAS."),
+ ("HVAC",
+ "Sistema de climatización, renovación de aire, ventilación."),
+ ("Escantillado ISO 12215-5",
+ "Espesores de paneles, cuadernas y estructuras según ISO 12215."),
+ ("Fabricación CNC — Nesting",
+ "Optimización de corte de planchas, generación de G-code."),
+ ("Moldes FRP",
+ "Lofting de moldes, schedule de laminado, BOM de materiales."),
+ ("Generador de Reportes",
+ "Exporta hidrostáticos, estabilidad, escantillado a PDF / Excel."),
+ ("Routing de Tuberías 3D",
+ "Trazado de tuberías en 3D dentro del casco "
+ "con cálculo hidráulico e isométricos automáticos."),
+ ("Routing de Cableados 3D",
+ "Trazado de bandejas y cables eléctricos con gestión "
+ "de conductores y cálculo de caídas de tensión."),
+ ]
+
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
- self.strings = strings
+
+ # Página 0: 4 viewports
+ self._four_vp = FourViewport()
+ self.addWidget(self._four_vp)
+
+ # Páginas 1..N: módulos de análisis/fabricación
+ for info in self._MODULE_INFO[1:]:
+ if info:
+ name, desc = info
+ self.addWidget(self._make_placeholder(name, desc))
+
+ self.setCurrentIndex(self.MOD_4VP)
+
+ @property
+ def four_viewport(self) -> FourViewport:
+ return self._four_vp
+
+ @staticmethod
+ def _make_placeholder(name: str, desc: str = "") -> QWidget:
+ w = QWidget()
+ w.setObjectName("modulePlaceholder")
+ lo = QVBoxLayout(w)
+ lo.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lo.setSpacing(10)
+
+ title = QLabel(name)
+ title.setObjectName("placeholderTitle")
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ f = title.font()
+ f.setPointSize(16)
+ f.setBold(True)
+ title.setFont(f)
+ lo.addWidget(title)
+
+ if desc:
+ desc_lbl = QLabel(desc)
+ desc_lbl.setObjectName("placeholderDesc")
+ desc_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ desc_lbl.setWordWrap(True)
+ desc_lbl.setMaximumWidth(520)
+ lo.addWidget(desc_lbl)
+
+ sub = QLabel("Módulo en desarrollo — se implementará en el sprint correspondiente.")
+ sub.setObjectName("placeholderMsg")
+ sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lo.addWidget(sub)
+ return w
+
+ def activate(self, module_index: int) -> None:
+ if 0 <= module_index < self.count():
+ self.setCurrentIndex(module_index)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# RIBBON BAR
+# ─────────────────────────────────────────────────────────────────────────────
+
+_RIBBON_TABS = [
+ "Home", "Geometría", "Análisis", "Tanques", "Sistemas", "Fabricación",
+]
+
+
+class RibbonGroup(QFrame):
+ """Un grupo de botones de la ribbon con etiqueta de categoría."""
+
+ def __init__(self, title: str, parent: Optional[QWidget] = None) -> None:
+ super().__init__(parent)
+ self.setObjectName("ribbonGroup")
+
+ lo = QVBoxLayout(self)
+ lo.setContentsMargins(3, 2, 3, 0)
+ lo.setSpacing(0)
+
+ self._btn_row = QWidget()
+ self._btn_row.setObjectName("ribbonBtnArea")
+ self._btn_lo = QHBoxLayout(self._btn_row)
+ self._btn_lo.setContentsMargins(0, 0, 0, 0)
+ self._btn_lo.setSpacing(2)
+
+ lbl = QLabel(title)
+ lbl.setObjectName("ribbonGroupTitle")
+ lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
+
+ lo.addWidget(self._btn_row)
+ lo.addWidget(lbl)
+
+ def add_button(
+ self,
+ icon: QIcon,
+ text: str,
+ tooltip: str,
+ slot=None,
+ enabled: bool = True,
+ ) -> QToolButton:
+ btn = QToolButton()
+ btn.setObjectName("ribbonButton")
+ btn.setIcon(icon)
+ btn.setText(text)
+ btn.setToolTip(tooltip)
+ btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
+ btn.setIconSize(QSize(22, 22))
+ btn.setEnabled(enabled)
+ if slot:
+ btn.clicked.connect(slot)
+ self._btn_lo.addWidget(btn)
+ return btn
+
+
+class RibbonBar(QWidget):
+ """
+ Ribbon bar con 6 pestañas funcionales.
+ Cada pestaña muestra grupos de botones relevantes para esa fase de trabajo.
+ """
+
+ TAB_HOME = 0
+ TAB_GEOMETRY = 1
+ TAB_ANALYSIS = 2
+ TAB_TANKS = 3
+ TAB_SYSTEMS = 4
+ TAB_FABRICATION = 5
+
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
+ super().__init__(parent)
+ self.setObjectName("ribbonBar")
+ self.setFixedHeight(92)
+ self._tab_btns: list[QToolButton] = []
self._build_ui()
def _build_ui(self) -> None:
- layout = QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
+ main = QVBoxLayout(self)
+ main.setContentsMargins(0, 0, 0, 0)
+ main.setSpacing(0)
- self.tabs = QTabWidget()
- self.tabs.setTabPosition(QTabWidget.TabPosition.North)
+ # ── Fila de pestañas ──────────────────────────────────────
+ tab_row = QWidget()
+ tab_row.setObjectName("ribbonTabRow")
+ tab_row.setFixedHeight(26)
+ trl = QHBoxLayout(tab_row)
+ trl.setContentsMargins(2, 0, 0, 0)
+ trl.setSpacing(0)
- 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 i, name in enumerate(_RIBBON_TABS):
+ btn = QToolButton()
+ btn.setText(name)
+ btn.setObjectName("ribbonTabBtn")
+ btn.setCheckable(True)
+ btn.setChecked(i == 0)
+ btn.setFixedHeight(26)
+ btn.setMinimumWidth(80)
+ btn.clicked.connect(lambda _=False, idx=i: self._select_tab(idx))
+ self._tab_btns.append(btn)
+ trl.addWidget(btn)
- for key, default_name in tab_names:
- name = self.strings.get(key, default_name)
- placeholder = self._make_placeholder(name)
- self.tabs.addTab(placeholder, name)
+ trl.addStretch()
+ main.addWidget(tab_row)
- layout.addWidget(self.tabs)
+ # ── Stack de contenido por pestaña ────────────────────────
+ self._stack = QStackedWidget()
+ self._stack.setObjectName("ribbonContent")
+ for _ in _RIBBON_TABS:
+ page = QWidget()
+ pl = QHBoxLayout(page)
+ pl.setContentsMargins(4, 2, 4, 0)
+ pl.setSpacing(0)
+ pl.setAlignment(Qt.AlignmentFlag.AlignLeft)
+ self._stack.addWidget(page)
- @staticmethod
- def _make_placeholder(tab_name: str) -> QWidget:
- w = QWidget()
- layout = QVBoxLayout(w)
- layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ main.addWidget(self._stack)
- icon_label = QLabel("🔜")
- icon_label.setFont(QFont("Segoe UI Emoji", 48))
- icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ def _select_tab(self, idx: int) -> None:
+ for i, btn in enumerate(self._tab_btns):
+ btn.setChecked(i == idx)
+ self._stack.setCurrentIndex(idx)
- 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
+ def new_group(self, tab_idx: int, title: str) -> RibbonGroup:
+ """Crea un grupo de botones en la pestaña indicada y devuelve el grupo."""
+ page_lo: QHBoxLayout = self._stack.widget(tab_idx).layout()
+ group = RibbonGroup(title)
+ page_lo.addWidget(group)
+ sep = QFrame()
+ sep.setObjectName("ribbonSep")
+ sep.setFrameShape(QFrame.Shape.VLine)
+ page_lo.addWidget(sep)
+ return group
-class NewProjectDialog(QMessageBox):
- """Dialog simple de nuevo proyecto para Sprint 0."""
- pass
-
+# ─────────────────────────────────────────────────────────────────────────────
+# VENTANA PRINCIPAL
+# ─────────────────────────────────────────────────────────────────────────────
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
+ Arquitectura:
+ - Ribbon bar con 6 pestañas funcionales (Home / Geometría / Análisis /
+ Tanques / Sistemas / Fabricación)
+ - Menú completo con submenús por módulo
+ - Dock izquierdo: panel de capas (LayersPanel)
+ - Área central: 4 viewports de modelado (default) o módulo de análisis
+ - Dock derecho: propiedades del elemento seleccionado
+ - Panel inferior fijo (36 px): hidrostáticos en vivo
+ - Barra de estado
"""
def __init__(self) -> None:
@@ -391,275 +795,400 @@ class MainWindow(QMainWindow):
self._lang = get_language()
self._strings = _load_i18n(self._lang)
self._setup_ui()
+ self._setup_ribbon()
self._setup_menu()
- self._setup_toolbar()
self._setup_status_bar()
self._restore_geometry()
self._update_title()
logger.info("MainWindow inicializada")
- # ──────────────────────────────────────────────
- # SETUP UI
- # ──────────────────────────────────────────────
+ # ─────────────────────────────────────────────────────────
+ # SETUP
+ # ─────────────────────────────────────────────────────────
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(
+ # Área central
+ self._module_area = ModuleArea()
+ self.setCentralWidget(self._module_area)
+
+ # Dock izquierdo — capas
+ self._layers_panel = LayersPanel(self._strings)
+ self._dock_layers = QDockWidget("Capas", self)
+ self._dock_layers.setObjectName("dockLayers")
+ self._dock_layers.setWidget(self._layers_panel)
+ self._dock_layers.setFeatures(
QDockWidget.DockWidgetFeature.DockWidgetMovable |
- QDockWidget.DockWidgetFeature.DockWidgetFloatable
+ QDockWidget.DockWidgetFeature.DockWidgetFloatable |
+ QDockWidget.DockWidgetFeature.DockWidgetClosable
)
- dock_tree.setMinimumWidth(200)
- self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock_tree)
+ self._dock_layers.setMinimumWidth(175)
+ self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self._dock_layers)
- # 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(
+ # Dock derecho — propiedades
+ self._props_panel = PropertiesPanel(self._strings)
+ self._dock_props = QDockWidget("Propiedades", self)
+ self._dock_props.setObjectName("dockProperties")
+ self._dock_props.setWidget(self._props_panel)
+ self._dock_props.setFeatures(
QDockWidget.DockWidgetFeature.DockWidgetMovable |
- QDockWidget.DockWidgetFeature.DockWidgetFloatable
+ QDockWidget.DockWidgetFeature.DockWidgetFloatable |
+ QDockWidget.DockWidgetFeature.DockWidgetClosable
)
- dock_props.setMinimumWidth(220)
- self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock_props)
+ self._dock_props.setMinimumWidth(200)
+ self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self._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 inferior — hidrostáticos (sin barra de título)
+ self._hydro = HydrostaticsPanel(self._strings)
+ dock_hydro = QDockWidget(self)
dock_hydro.setObjectName("dockHydrostatics")
- dock_hydro.setWidget(self._hydro_panel)
+ dock_hydro.setWidget(self._hydro)
dock_hydro.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures)
- dock_hydro.setTitleBarWidget(QWidget()) # Oculta la barra de título del dock
+ dock_hydro.setTitleBarWidget(QWidget())
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, dock_hydro)
- # Conectar señales
- self._project_tree.item_selected.connect(self._on_tree_item_selected)
+ def _setup_ribbon(self) -> None:
+ sp = QStyle.StandardPixmap
+ M = ModuleArea
+
+ self._ribbon = RibbonBar()
+
+ # Envuelto en QToolBar para integración correcta con QMainWindow
+ tb = QToolBar("Ribbon", self)
+ tb.setObjectName("ribbonToolbar")
+ tb.setMovable(False)
+ tb.setFloatable(False)
+ tb.setContentsMargins(0, 0, 0, 0)
+ tb.addWidget(self._ribbon)
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb)
+
+ # ── Tab HOME ─────────────────────────────────────────────
+ g = self._ribbon.new_group(RibbonBar.TAB_HOME, "Archivo")
+ g.add_button(_spi(sp.SP_FileIcon), "Nuevo", "Nuevo proyecto Ctrl+N",
+ self._on_new_project)
+ g.add_button(_spi(sp.SP_DirOpenIcon), "Abrir", "Abrir proyecto Ctrl+O",
+ self._on_open_project)
+ g.add_button(_spi(sp.SP_DialogSaveButton), "Guardar", "Guardar Ctrl+S",
+ self._on_save_project)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_HOME, "Editar")
+ g.add_button(_spi(sp.SP_ArrowBack), "Deshacer", "Deshacer Ctrl+Z", enabled=False)
+ g.add_button(_spi(sp.SP_ArrowForward), "Rehacer", "Rehacer Ctrl+Y", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_HOME, "Vistas")
+ g.add_button(_spi(sp.SP_DesktopIcon), "4 Vistas", "4 Viewports F2",
+ lambda: self._module_area.activate(M.MOD_4VP))
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Líneas", "Plano de Líneas F3",
+ lambda: self._module_area.activate(M.MOD_LINES))
+ g.add_button(_spi(sp.SP_FileDialogListView), "Offsets", "Tabla de Offsets F4",
+ lambda: self._module_area.activate(M.MOD_OFFSETS))
+
+ # ── Tab GEOMETRÍA ─────────────────────────────────────────
+ g = self._ribbon.new_group(RibbonBar.TAB_GEOMETRY, "Nuevo")
+ g.add_button(_spi(sp.SP_FileIcon), "Asistente", "Asistente de embarcación", enabled=False)
+ g.add_button(_spi(sp.SP_FileIcon), "Casco NURBS", "Nuevo casco NURBS", enabled=False)
+ g.add_button(_spi(sp.SP_FileIcon), "Apéndice", "Añadir apéndice", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_GEOMETRY, "Edición NURBS")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Pts. Ctrl.", "Editar puntos de control", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Extruir", "Extruir sección", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Espejo", "Espejo por eje de crujía", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Lackenby", "Transformación de Lackenby", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_GEOMETRY, "Importar")
+ g.add_button(_spi(sp.SP_DirOpenIcon), "Offsets", "Importar tabla de offsets (.txt / .csv)", enabled=False)
+ g.add_button(_spi(sp.SP_DirOpenIcon), "DXF", "Importar plano DXF", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_GEOMETRY, "Exportar")
+ g.add_button(_spi(sp.SP_DialogSaveButton), "IGES", "Exportar IGES", enabled=False)
+ g.add_button(_spi(sp.SP_DialogSaveButton), "STEP", "Exportar STEP", enabled=False)
+ g.add_button(_spi(sp.SP_DialogSaveButton), "DXF", "Exportar DXF", enabled=False)
+
+ # ── Tab ANÁLISIS ──────────────────────────────────────────
+ g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Hidrostática")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Calcular", "Calcular hidrostáticos", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Curvas", "Curvas hidrostáticas",
+ lambda: self._module_area.activate(M.MOD_CURVES), False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Estabilidad")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Curva GZ", "Curva GZ estática",
+ lambda: self._module_area.activate(M.MOD_STABILITY), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "IMO IS2008", "Criterios IMO IS Code 2008", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Avería", "Estabilidad en avería", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Resistencia")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Holtrop", "Holtrop & Mennen",
+ lambda: self._module_area.activate(M.MOD_RESISTANCE), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Savitsky", "Savitsky (planeo)", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "VPP", "VPP Velero / DSYHS",
+ lambda: self._module_area.activate(M.MOD_VPP), False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Seakeeping")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "STF", "Strip Theory (STF)",
+ lambda: self._module_area.activate(M.MOD_SEAKEEPING), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Espectro", "Espectro de respuesta", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Estructura")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "ISO 12215", "Escantillado ISO 12215-5",
+ lambda: self._module_area.activate(M.MOD_SCANTLING), False)
+
+ # ── Tab TANQUES ───────────────────────────────────────────
+ g = self._ribbon.new_group(RibbonBar.TAB_TANKS, "Tanques")
+ g.add_button(_spi(sp.SP_FileIcon), "Nuevo Tq.", "Definir nuevo tanque",
+ lambda: self._module_area.activate(M.MOD_TANKS), False)
+ g.add_button(_spi(sp.SP_FileIcon), "Modelar", "Modelar tanque NURBS", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_TANKS, "Casos de Carga")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Nuevo caso", "Definir caso de carga", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Sondeos", "Tablas de sondeo",
+ lambda: self._module_area.activate(M.MOD_CAPACITY), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Calc. KG", "Calcular KG por caso", enabled=False)
+
+ # ── Tab SISTEMAS ──────────────────────────────────────────
+ g = self._ribbon.new_group(RibbonBar.TAB_SYSTEMS, "Eléctrico")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "EPLA", "Balance eléctrico (EPLA)",
+ lambda: self._module_area.activate(M.MOD_ELECTRICAL), False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_SYSTEMS, "Fluidos")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Combustible", "Sistema de combustible",
+ lambda: self._module_area.activate(M.MOD_FUEL), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Agua Dulce", "Sistema de agua dulce",
+ lambda: self._module_area.activate(M.MOD_FRESHWATER), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Achique", "Sistema de achique",
+ lambda: self._module_area.activate(M.MOD_BILGE), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "C. Incendio", "Sistema contra incendios",
+ lambda: self._module_area.activate(M.MOD_FIREFIGHT), False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_SYSTEMS, "Routing 3D")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Tuberías", "Routing de tuberías 3D",
+ lambda: self._module_area.activate(M.MOD_ROUTING_PIPES), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Cableados", "Routing de cableados 3D",
+ lambda: self._module_area.activate(M.MOD_ROUTING_CABLES), False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_SYSTEMS, "Clima / Control")
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "HVAC", "Sistema HVAC",
+ lambda: self._module_area.activate(M.MOD_HVAC), False)
+ g.add_button(_spi(sp.SP_FileDialogDetailedView), "Gobierno", "Sistema de gobierno", enabled=False)
+
+ # ── Tab FABRICACIÓN ───────────────────────────────────────
+ g = self._ribbon.new_group(RibbonBar.TAB_FABRICATION, "CNC")
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "Materiales", "Estimación de materiales", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "Nesting", "Optimización de cortes (nesting)",
+ lambda: self._module_area.activate(M.MOD_CNC), False)
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "G-code", "Generar G-code", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "Post-Proc.", "Configurar post-procesador CNC", enabled=False)
+
+ g = self._ribbon.new_group(RibbonBar.TAB_FABRICATION, "Moldes FRP")
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "Lofting", "Lofting del molde",
+ lambda: self._module_area.activate(M.MOD_MOLDS), False)
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "Laminado", "Schedule de laminado", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "Resina", "Calculadora de resina", enabled=False)
+ g.add_button(_spi(sp.SP_FileDialogContentsView), "BOM", "BOM de materiales", enabled=False)
def _setup_menu(self) -> None:
- s = self._strings
- menubar = self.menuBar()
+ mb = self.menuBar()
+ sp = QStyle.StandardPixmap
+ M = ModuleArea
- # ── 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"))
+ # ── ARCHIVO ────────────────────────────────────────────────
+ m = mb.addMenu("Archivo")
+ self._add_action(m, "Nuevo proyecto", QKeySequence.StandardKey.New, self._on_new_project, _spi(sp.SP_FileIcon))
+ self._add_action(m, "Abrir...", QKeySequence.StandardKey.Open, self._on_open_project, _spi(sp.SP_DirOpenIcon))
+ m.addSeparator()
+ self._add_action(m, "Guardar", QKeySequence.StandardKey.Save, self._on_save_project, _spi(sp.SP_DialogSaveButton))
+ self._add_action(m, "Guardar como...", QKeySequence("Ctrl+Shift+S"), self._on_save_as)
+ m.addSeparator()
+ self._recent_menu = m.addMenu("Recientes")
self._update_recent_menu()
+ m.addSeparator()
+ self._add_action(m, "Salir", QKeySequence("Alt+F4"), self.close)
- menu_file.addSeparator()
+ # ── EDITAR ─────────────────────────────────────────────────
+ m = mb.addMenu("Editar")
+ self._act_undo = self._add_action(m, "Deshacer", QKeySequence.StandardKey.Undo, enabled=False)
+ self._act_redo = self._add_action(m, "Rehacer", QKeySequence.StandardKey.Redo, enabled=False)
+ m.addSeparator()
+ self._add_action(m, "Preferencias...", slot=self._on_preferences)
- 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)
+ # ── VER ────────────────────────────────────────────────────
+ m = mb.addMenu("Ver")
+ self._add_action(m, "4 Viewports", QKeySequence("F2"), lambda: self._module_area.activate(M.MOD_4VP))
+ self._add_action(m, "Plano de Líneas", QKeySequence("F3"), lambda: self._module_area.activate(M.MOD_LINES))
+ self._add_action(m, "Tabla de Offsets", QKeySequence("F4"), lambda: self._module_area.activate(M.MOD_OFFSETS))
+ m.addSeparator()
+ self._add_action(m, "Panel Capas",
+ slot=lambda: self._dock_layers.setVisible(not self._dock_layers.isVisible()))
+ self._add_action(m, "Panel Propiedades",
+ slot=lambda: self._dock_props.setVisible(not self._dock_props.isVisible()))
+ m.addSeparator()
+ self._add_action(m, "Tema claro / oscuro", slot=self._on_toggle_theme)
- # ── EDITAR ──
- menu_edit = menubar.addMenu(s.get("menu_edit", "Editar"))
+ # ── MODELO ────────────────────────────────────────────────
+ m = mb.addMenu("Modelo")
+ self._add_action(m, "Asistente de embarcación...", enabled=False)
+ self._add_action(m, "Nuevo casco NURBS...", enabled=False)
+ m.addSeparator()
+ self._add_action(m, "Importar offsets (.txt / .csv)...", enabled=False)
+ self._add_action(m, "Importar DXF...", enabled=False)
+ self._add_action(m, "Exportar IGES...", enabled=False)
+ self._add_action(m, "Exportar STEP...", enabled=False)
- 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)
+ # ── ANÁLISIS ──────────────────────────────────────────────
+ m = mb.addMenu("Análisis")
- 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)
+ sm = m.addMenu("Hidrostática")
+ self._add_action(sm, "Calcular hidrostáticos", enabled=False)
+ self._add_action(sm, "Curvas hidrostáticas", slot=lambda: self._module_area.activate(M.MOD_CURVES), enabled=False)
- menu_edit.addSeparator()
+ sm = m.addMenu("Estabilidad")
+ self._add_action(sm, "Curva GZ — Estabilidad estática", slot=lambda: self._module_area.activate(M.MOD_STABILITY), enabled=False)
+ self._add_action(sm, "Criterios IMO IS Code 2008", enabled=False)
+ self._add_action(sm, "Criterio de viento A.749(18)", enabled=False)
+ self._add_action(sm, "Estabilidad en avería (SOLAS 2009)", enabled=False)
- act_prefs = QAction(s.get("edit_preferences", "Preferencias..."), self)
- act_prefs.triggered.connect(self._on_preferences)
- menu_edit.addAction(act_prefs)
+ sm = m.addMenu("Resistencia y Propulsión")
+ self._add_action(sm, "Resistencia (Holtrop & Mennen / Savitsky)", slot=lambda: self._module_area.activate(M.MOD_RESISTANCE), enabled=False)
+ self._add_action(sm, "Propulsión — Wageningen B-series", slot=lambda: self._module_area.activate(M.MOD_PROPULSION), enabled=False)
+ self._add_action(sm, "VPP Velero (DSYHS)", slot=lambda: self._module_area.activate(M.MOD_VPP), enabled=False)
- # ── 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)
+ sm = m.addMenu("Seakeeping")
+ self._add_action(sm, "Movimientos (Strip Theory STF)", slot=lambda: self._module_area.activate(M.MOD_SEAKEEPING), enabled=False)
+ self._add_action(sm, "Espectro de respuesta", enabled=False)
- # ── 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)")
+ m.addSeparator()
+ self._add_action(m, "Escantillado ISO 12215-5", slot=lambda: self._module_area.activate(M.MOD_SCANTLING), enabled=False)
- # ── 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)")
+ # ── TANQUES Y CARGA ────────────────────────────────────────
+ m = mb.addMenu("Tanques")
+ self._add_action(m, "Modelado de tanques", slot=lambda: self._module_area.activate(M.MOD_TANKS), enabled=False)
+ self._add_action(m, "Plan de capacidad", slot=lambda: self._module_area.activate(M.MOD_CAPACITY), enabled=False)
+ self._add_action(m, "Tablas de sondeo", enabled=False)
+ self._add_action(m, "Casos de carga / KG", enabled=False)
- # ── 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)
+ # ── SISTEMAS ──────────────────────────────────────────────
+ m = mb.addMenu("Sistemas")
+ self._add_action(m, "Balance eléctrico (EPLA)", slot=lambda: self._module_area.activate(M.MOD_ELECTRICAL), enabled=False)
+ self._add_action(m, "Sistema de combustible", slot=lambda: self._module_area.activate(M.MOD_FUEL), enabled=False)
+ self._add_action(m, "Sistema de agua dulce", slot=lambda: self._module_area.activate(M.MOD_FRESHWATER), enabled=False)
+ self._add_action(m, "Sistema de achique", slot=lambda: self._module_area.activate(M.MOD_BILGE), enabled=False)
+ m.addSeparator()
+ self._add_action(m, "Sistema contra incendios", slot=lambda: self._module_area.activate(M.MOD_FIREFIGHT), enabled=False)
+ self._add_action(m, "HVAC", slot=lambda: self._module_area.activate(M.MOD_HVAC), enabled=False)
+ self._add_action(m, "Sistema de gobierno", enabled=False)
+ self._add_action(m, "Lastre y amarre", enabled=False)
+ m.addSeparator()
+ sm = m.addMenu("Routing 3D")
+ self._add_action(sm, "Routing de tuberías", slot=lambda: self._module_area.activate(M.MOD_ROUTING_PIPES), enabled=False)
+ self._add_action(sm, "Routing de cableados", slot=lambda: self._module_area.activate(M.MOD_ROUTING_CABLES), enabled=False)
- # ── 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)")
+ # ── FABRICACIÓN ────────────────────────────────────────────
+ m = mb.addMenu("Fabricación")
+ sm = m.addMenu("CNC")
+ self._add_action(sm, "Estimación de materiales", enabled=False)
+ self._add_action(sm, "Optimización de cortes (Nesting)", slot=lambda: self._module_area.activate(M.MOD_CNC), enabled=False)
+ self._add_action(sm, "Generar G-code", enabled=False)
+ self._add_action(sm, "Configurar post-procesador CNC", enabled=False)
- # ── 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)")
+ sm = m.addMenu("Moldes FRP")
+ self._add_action(sm, "Lofting del molde", slot=lambda: self._module_area.activate(M.MOD_MOLDS), enabled=False)
+ self._add_action(sm, "Schedule de laminado", enabled=False)
+ self._add_action(sm, "Calculadora de resina", enabled=False)
+ self._add_action(sm, "BOM de materiales", enabled=False)
- # ── 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)
+ # ── REPORTES ───────────────────────────────────────────────
+ m = mb.addMenu("Reportes")
+ self._add_action(m, "Reporte hidrostático", slot=lambda: self._module_area.activate(M.MOD_REPORT), enabled=False)
+ self._add_action(m, "Cuaderno de estabilidad", enabled=False)
+ self._add_action(m, "Plano de líneas (PDF / DXF)", enabled=False)
+ self._add_action(m, "Reporte de escantillado", enabled=False)
+ self._add_action(m, "Balance eléctrico (Excel)", enabled=False)
+ self._add_action(m, "Plan de capacidad", enabled=False)
- def _setup_toolbar(self) -> None:
- tb = QToolBar("Principal", self)
- tb.setObjectName("mainToolbar")
- tb.setMovable(False)
- self.addToolBar(tb)
+ # ── AYUDA ─────────────────────────────────────────────────
+ m = mb.addMenu("Ayuda")
+ self._add_action(m, "Acerca de AR-ShipDesign...", slot=self._on_about)
- 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 _add_action(
+ self,
+ menu,
+ text: str,
+ shortcut=None,
+ slot=None,
+ icon: Optional[QIcon] = None,
+ enabled: bool = True,
+ ) -> QAction:
+ a = QAction(text, self)
+ if icon:
+ a.setIcon(icon)
+ if shortcut:
+ a.setShortcut(shortcut)
+ if slot:
+ a.triggered.connect(slot)
+ a.setEnabled(enabled)
+ menu.addAction(a)
+ return a
def _setup_status_bar(self) -> None:
sb = self.statusBar()
- sb.showMessage(self._strings.get("status_ready", "Listo"))
+ sb.showMessage("Listo")
- self._status_version = QLabel(f" AR-ShipDesign v{__version__} ")
- sb.addPermanentWidget(self._status_version)
+ # Unidades
+ lbl_u = QLabel("Unidades: ")
+ lbl_u.setObjectName("statusLabel")
+ self._units_lbl = QLabel("SI")
+ self._units_lbl.setObjectName("statusValue")
- # ──────────────────────────────────────────────
+ # Idioma
+ lbl_l = QLabel(" Idioma: ")
+ lbl_l.setObjectName("statusLabel")
+ self._lang_lbl = QLabel(self._lang.upper())
+ self._lang_lbl.setObjectName("statusValue")
+
+ # Versión
+ ver = QLabel(f" AR-ShipDesign v{__version__} ")
+ ver.setObjectName("statusVersion")
+
+ sb.addPermanentWidget(lbl_u)
+ sb.addPermanentWidget(self._units_lbl)
+ sb.addPermanentWidget(lbl_l)
+ sb.addPermanentWidget(self._lang_lbl)
+ sb.addPermanentWidget(ver)
+
+ # ─────────────────────────────────────────────────────────
# ACCIONES DE PROYECTO
- # ──────────────────────────────────────────────
+ # ─────────────────────────────────────────────────────────
def _on_new_project(self) -> None:
- if self._project and self._project.is_modified:
- if not self._ask_save_changes():
- return
+ if self._project and self._project.is_modified and not self._ask_save():
+ 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
-
+ if self._project and self._project.is_modified and not self._ask_save():
+ return
path, _ = QFileDialog.getOpenFileName(
- self,
- "Abrir proyecto AR-ShipDesign",
- str(Path.home()),
+ self, "Abrir proyecto", 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}")
+ self.statusBar().showMessage(f"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)
+ QMessageBox.critical(self, "Error al abrir", str(e))
def _on_save_project(self) -> None:
- if self._project is None:
+ if not self._project:
return
- if self._project.path is None:
- self._on_save_as_project()
+ if not self._project.path:
+ self._on_save_as()
return
try:
self._project.save()
@@ -668,12 +1197,11 @@ class MainWindow(QMainWindow):
except Exception as e:
QMessageBox.critical(self, "Error al guardar", str(e))
- def _on_save_as_project(self) -> None:
- if self._project is None:
+ def _on_save_as(self) -> None:
+ if not self._project:
return
path, _ = QFileDialog.getSaveFileName(
- self,
- "Guardar proyecto como...",
+ self, "Guardar como...",
str(Path.home() / f"{self._project.name}.arsd"),
"Proyectos AR-ShipDesign (*.arsd)",
)
@@ -689,59 +1217,52 @@ class MainWindow(QMainWindow):
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)
+ self._layers_panel.set_project(self._project)
- def _ask_save_changes(self) -> bool:
- """Pregunta si guardar antes de cerrar/nuevo. Retorna True si se puede continuar."""
+ def _ask_save(self) -> bool:
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,
+ self, "Cambios sin guardar",
+ f"'{self._project.name}' tiene cambios sin guardar. ¿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
+ return reply == QMessageBox.StandardButton.Discard
- # ──────────────────────────────────────────────
+ # ─────────────────────────────────────────────────────────
# 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")
+ QMessageBox.information(self, "Preferencias", "Disponible en 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"
+ qss_path = Path(__file__).parent / "themes" / f"{new_theme}.qss"
try:
- qss = qss_path.read_text(encoding="utf-8")
- QApplication.instance().setStyleSheet(qss)
+ QApplication.instance().setStyleSheet(
+ qss_path.read_text(encoding="utf-8")
+ )
except Exception as e:
- logger.warning("No se pudo aplicar el tema %s: %s", theme, e)
+ logger.warning("Tema %s no disponible: %s", new_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")}""",
+ self, "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
"
+ "Estabilidad: IMO IS Code 2008 · A.749(18) · SOLAS 2009
"
+ "Resistencia: Holtrop & Mennen · Savitsky · DSYHS
"
+ "Escantillado: ISO 12215-5
"
+ f"{self._strings.get('about_copyright', 'Copyright © 2025 Álvaro Romero')}",
)
def _update_title(self) -> None:
@@ -754,34 +1275,33 @@ Estándares: ISO 12215, IMO IS Code 2008
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)
+ a = QAction("(sin archivos recientes)", self)
+ a.setEnabled(False)
+ self._recent_menu.addAction(a)
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)
+ a = QAction(Path(path).name, self)
+ a.setToolTip(path)
+ a.triggered.connect(lambda _=False, p=path: self._open_recent(p))
+ self._recent_menu.addAction(a)
def _open_recent(self, path: str) -> None:
- if self._project and self._project.is_modified:
- if not self._ask_save_changes():
- return
+ if self._project and self._project.is_modified and not self._ask_save():
+ 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}")
+ QMessageBox.critical(self, "Error", str(e))
- # ──────────────────────────────────────────────
- # GEOMETRÍA Y ESTADO
- # ──────────────────────────────────────────────
+ # ─────────────────────────────────────────────────────────
+ # GEOMETRÍA DE VENTANA
+ # ─────────────────────────────────────────────────────────
def _restore_geometry(self) -> None:
s = get_settings()
- geom = s.value("ui/windowGeometry")
+ geom = s.value("ui/windowGeometry")
state = s.value("ui/windowState")
if geom:
self.restoreGeometry(geom)
@@ -792,10 +1312,9 @@ Estándares: ISO 12215, IMO IS Code 2008
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
+ if self._project and self._project.is_modified and not self._ask_save():
+ event.ignore()
+ return
s = get_settings()
s.setValue("ui/windowGeometry", self.saveGeometry())
s.setValue("ui/windowState", self.saveState())
diff --git a/arshipdesign/ui/themes/dark.qss b/arshipdesign/ui/themes/dark.qss
index 43f7948..f93ed8a 100644
--- a/arshipdesign/ui/themes/dark.qss
+++ b/arshipdesign/ui/themes/dark.qss
@@ -1,162 +1,616 @@
-/* AR-ShipDesign — Tema Oscuro */
+/* ============================================================
+ AR-ShipDesign — Tema Oscuro Profesional
+ Estilo: ingeniería CAD / naval / Office dark mode
+ Paleta: #1e2128 fondo, #252830 panel, #2d3139 elevado,
+ #0d99ff acento, #e8eaf0 texto, #8b919e texto muted
+ ============================================================ */
+
+/* ─── BASE ─────────────────────────────────────────────────── */
+
+* {
+ outline: none;
+}
QMainWindow, QDialog, QWidget {
background-color: #1e2128;
- color: #e0e0e0;
+ color: #e8eaf0;
font-family: "Segoe UI", Arial, sans-serif;
font-size: 13px;
}
+/* ─── MENÚ ──────────────────────────────────────────────────── */
+
QMenuBar {
background-color: #252830;
- color: #e0e0e0;
- border-bottom: 1px solid #3a3f4b;
+ color: #e8eaf0;
+ border-bottom: 1px solid #333841;
+ padding: 1px 0;
+}
+QMenuBar::item {
+ padding: 4px 10px;
+ background: transparent;
+}
+QMenuBar::item:selected {
+ background-color: #2d3139;
+ color: #ffffff;
+}
+QMenuBar::item:pressed {
+ background-color: #0d99ff;
+ color: #ffffff;
}
-QMenuBar::item:selected { background-color: #3a4050; }
QMenu {
- background-color: #2c313a;
- color: #e0e0e0;
- border: 1px solid #3a3f4b;
-}
-QMenu::item:selected { background-color: #1565c0; }
-QMenu::separator { height: 1px; background: #3a3f4b; }
-
-QToolBar {
background-color: #252830;
- border-bottom: 1px solid #3a3f4b;
- spacing: 4px;
- padding: 2px;
+ color: #e8eaf0;
+ border: 1px solid #3a3f4b;
+ padding: 3px 0;
}
-QToolButton {
- background: transparent;
- border: 1px solid transparent;
- border-radius: 3px;
- padding: 3px;
- color: #e0e0e0;
+QMenu::item {
+ padding: 5px 28px 5px 20px;
+}
+QMenu::item:selected {
+ background-color: #0d99ff;
+ color: #ffffff;
+}
+QMenu::item:disabled {
+ color: #555b67;
+}
+QMenu::separator {
+ height: 1px;
+ background: #333841;
+ margin: 3px 8px;
}
-QToolButton:hover { background-color: #3a4050; border-color: #5a6070; }
-QToolButton:pressed { background-color: #1565c0; }
-QSplitter::handle { background-color: #3a3f4b; }
-QSplitter::handle:horizontal { width: 3px; }
-QSplitter::handle:vertical { height: 3px; }
+/* ─── RIBBON TOOLBAR (contenedor) ───────────────────────────── */
-QTreeWidget, QTreeView, QListView, QTableView, QTableWidget {
+QToolBar#ribbonToolbar {
+ background-color: #252830;
+ border: none;
+ border-bottom: 1px solid #333841;
+ padding: 0;
+ spacing: 0;
+ margin: 0;
+}
+
+/* ─── RIBBON BAR ────────────────────────────────────────────── */
+
+QWidget#ribbonBar {
+ background-color: #252830;
+}
+
+/* Fila de pestañas */
+QWidget#ribbonTabRow {
background-color: #1e2128;
- alternate-background-color: #242830;
- color: #e0e0e0;
- border: 1px solid #3a3f4b;
- gridline-color: #3a3f4b;
- selection-background-color: #1565c0;
- selection-color: #ffffff;
+ border-bottom: 1px solid #333841;
}
-QTreeWidget::item:hover, QTreeView::item:hover { background-color: #2a2f3a; }
-QHeaderView::section {
- background-color: #2c313a;
- color: #b0b8c8;
- border: 1px solid #3a3f4b;
- padding: 4px 6px;
+
+/* Botones de pestañas */
+QToolButton#ribbonTabBtn {
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ border-radius: 0;
+ padding: 0 14px;
+ color: #8b919e;
+ font-size: 12px;
+ min-width: 80px;
+ height: 26px;
+}
+QToolButton#ribbonTabBtn:hover {
+ background-color: #2d3139;
+ color: #c8cad0;
+}
+QToolButton#ribbonTabBtn:checked {
+ background-color: #252830;
+ color: #0d99ff;
+ border-bottom: 2px solid #0d99ff;
font-weight: bold;
}
-QTabWidget::pane {
- border: 1px solid #3a3f4b;
- background-color: #1e2128;
-}
-QTabBar::tab {
+/* Stack de contenido del ribbon */
+QStackedWidget#ribbonContent {
background-color: #252830;
- color: #a0a8b8;
- border: 1px solid #3a3f4b;
- border-bottom: none;
- padding: 5px 10px;
- margin-right: 1px;
-}
-QTabBar::tab:selected {
- background-color: #1e2128;
- color: #ffffff;
- border-bottom: 2px solid #1976d2;
-}
-QTabBar::tab:hover { background-color: #2c313a; color: #e0e0e0; }
-
-QScrollBar:vertical {
- background-color: #1e2128;
- width: 10px;
-}
-QScrollBar::handle:vertical {
- background-color: #4a4f5b;
- border-radius: 4px;
- min-height: 20px;
-}
-QScrollBar::handle:vertical:hover { background-color: #5a6070; }
-QScrollBar:horizontal {
- background-color: #1e2128;
- height: 10px;
-}
-QScrollBar::handle:horizontal {
- background-color: #4a4f5b;
- border-radius: 4px;
- min-width: 20px;
}
-QPushButton {
- background-color: #1976d2;
- color: #ffffff;
- border: none;
- border-radius: 4px;
- padding: 6px 14px;
- font-weight: 500;
-}
-QPushButton:hover { background-color: #1e88e5; }
-QPushButton:pressed { background-color: #1565c0; }
-QPushButton:disabled { background-color: #3a3f4b; color: #6a7080; }
-
-QPushButton[secondary="true"] {
+/* Grupo individual */
+QFrame#ribbonGroup {
background-color: transparent;
- color: #90caf9;
- border: 1px solid #1976d2;
+ border: none;
+ margin: 2px 0;
}
-QPushButton[secondary="true"]:hover { background-color: #1a2a3a; }
-QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {
- background-color: #2c313a;
- color: #e0e0e0;
- border: 1px solid #4a4f5b;
+/* Área de botones dentro del grupo */
+QWidget#ribbonBtnArea {
+ background-color: transparent;
+}
+
+/* Etiqueta del nombre del grupo */
+QLabel#ribbonGroupTitle {
+ color: #8b919e;
+ font-size: 10px;
+ padding: 2px 0 3px 0;
+ background-color: transparent;
+}
+
+/* Separador vertical entre grupos */
+QFrame#ribbonSep {
+ color: #2d3139;
+ margin: 6px 2px;
+}
+
+/* Botones de la ribbon */
+QToolButton#ribbonButton {
+ background: transparent;
+ color: #e8eaf0;
+ border: 1px solid transparent;
border-radius: 3px;
- padding: 4px 6px;
+ padding: 4px 6px 2px 6px;
+ font-size: 11px;
+ min-width: 46px;
+ max-width: 72px;
}
-QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {
- border-color: #1976d2;
+QToolButton#ribbonButton:hover {
+ background-color: #2d3139;
+ border-color: #3a3f4b;
+}
+QToolButton#ribbonButton:pressed {
+ background-color: #0d99ff;
+ color: #ffffff;
+ border-color: #0d99ff;
+}
+QToolButton#ribbonButton:checked {
+ background-color: #1a4a70;
+ border-color: #0d99ff;
+}
+QToolButton#ribbonButton:disabled {
+ color: #555b67;
}
-QComboBox::drop-down { border: none; }
-QComboBox::down-arrow { image: none; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #a0a8b8; margin-right: 6px; }
-QComboBox QAbstractItemView { background-color: #2c313a; color: #e0e0e0; selection-background-color: #1565c0; }
+/* ─── 4 VIEWPORTS ───────────────────────────────────────────── */
-QLabel { color: #e0e0e0; }
-QLabel[heading="true"] { font-size: 14px; font-weight: bold; color: #90caf9; }
-QLabel[muted="true"] { color: #7a8090; }
+QWidget#fourViewport {
+ background-color: #12151a;
+}
+
+QFrame#viewportFrame {
+ background-color: #12151a;
+ border: 1px solid #1e2128;
+}
+
+QWidget#viewportTitleBar {
+ background-color: #1a1e26;
+ border-bottom: 1px solid #2a2e38;
+}
+
+QLabel#viewportTitle {
+ color: #6b7280;
+ font-size: 11px;
+ font-weight: bold;
+ letter-spacing: 0.5px;
+}
+
+QWidget#viewportCanvas {
+ background-color: #0e1117;
+}
+
+QLabel#viewportPlaceholder {
+ color: #1e2540;
+ font-size: 20px;
+ font-family: "Consolas", monospace;
+ font-weight: bold;
+ letter-spacing: 2px;
+}
+
+/* Separadores arrastrables del viewport */
+QSplitter#viewportSplitter::handle {
+ background-color: #0d99ff;
+}
+QSplitter#viewportSplitter::handle:horizontal {
+ width: 3px;
+}
+QSplitter#viewportSplitter::handle:vertical {
+ height: 3px;
+}
+QSplitter#viewportSplitter::handle:hover {
+ background-color: #3db8ff;
+}
+
+/* ─── DOCK WIDGETS ──────────────────────────────────────────── */
+
+QDockWidget {
+ color: #e8eaf0;
+ font-weight: bold;
+ font-size: 12px;
+}
+QDockWidget::title {
+ background-color: #252830;
+ padding: 4px 8px;
+ border-bottom: 1px solid #333841;
+ text-align: left;
+}
+QDockWidget::close-button,
+QDockWidget::float-button {
+ background: transparent;
+ border: none;
+ padding: 2px;
+}
+QDockWidget::close-button:hover,
+QDockWidget::float-button:hover {
+ background-color: #3a3f4b;
+ border-radius: 2px;
+}
+
+/* ─── PANEL DE CAPAS ────────────────────────────────────────── */
+
+QWidget#layersPanel {
+ background-color: #1e2128;
+}
+
+QWidget#layersPanelHeader {
+ background-color: #252830;
+ border-bottom: 1px solid #333841;
+}
+
+QFrame#panelSep {
+ color: #333841;
+ max-height: 1px;
+}
+
+QScrollArea#layersScrollArea {
+ background-color: #1e2128;
+ border: none;
+}
+
+QWidget#layersContainer {
+ background-color: #1e2128;
+}
+
+QWidget#layerRow {
+ background-color: transparent;
+ min-height: 26px;
+}
+QWidget#layerRow:hover {
+ background-color: #252830;
+}
+
+QLabel#layerName {
+ color: #c8cad0;
+ font-size: 12px;
+}
+
+/* Botón de visibilidad de capa */
+QToolButton#layerVisBtn {
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ color: #0d99ff;
+ font-size: 9px;
+ font-weight: bold;
+}
+QToolButton#layerVisBtn:hover {
+ background-color: #2d3139;
+ border-color: #3a3f4b;
+}
+QToolButton#layerVisBtn:checked {
+ background-color: #1a4a70;
+ border-color: #0d99ff;
+}
+
+/* Botón de bloqueo de capa */
+QToolButton#layerLockBtn {
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ color: #fbbf24;
+ font-size: 9px;
+}
+QToolButton#layerLockBtn:hover {
+ background-color: #2d3139;
+ border-color: #3a3f4b;
+}
+QToolButton#layerLockBtn:checked {
+ background-color: #3d2c00;
+ border-color: #fbbf24;
+}
+
+/* ─── PROPIEDADES ───────────────────────────────────────────── */
+
+QWidget#propContent {
+ background-color: #1e2128;
+}
+QLabel#propSection {
+ color: #8b919e;
+ font-size: 11px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid #333841;
+ margin-bottom: 4px;
+}
+QLabel#propKey {
+ color: #8b919e;
+ font-size: 12px;
+}
+QLabel#propVal {
+ color: #e8eaf0;
+ font-size: 13px;
+}
+
+/* ─── PANEL HIDROSTÁTICOS ───────────────────────────────────── */
+
+QFrame#hydrostaticsPanel {
+ background-color: #14181f;
+ border-top: 1px solid #333841;
+}
+QLabel#hydroTitle {
+ color: #555b67;
+ font-size: 10px;
+ font-weight: bold;
+ padding: 0 10px 0 4px;
+ letter-spacing: 1px;
+}
+QFrame#hydroSep {
+ color: #333841;
+}
+QLabel#hydroKey {
+ color: #8b919e;
+ font-size: 11px;
+ padding: 0 2px;
+}
+QLabel#hydroVal {
+ color: #0d99ff;
+ font-family: "Consolas", monospace;
+ font-size: 12px;
+ min-width: 52px;
+}
+QLabel#hydroImoNone {
+ color: #555b67;
+ font-family: "Consolas", monospace;
+ font-size: 12px;
+}
+QLabel#hydroImoOk {
+ color: #34c759;
+ font-family: "Consolas", monospace;
+ font-size: 12px;
+ font-weight: bold;
+}
+QLabel#hydroImoFail {
+ color: #ff3b30;
+ font-family: "Consolas", monospace;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+/* ─── MÓDULOS PLACEHOLDER ───────────────────────────────────── */
+
+QWidget#modulePlaceholder {
+ background-color: #1e2128;
+}
+
+QLabel#placeholderTitle {
+ color: #2d3448;
+ font-size: 18px;
+ font-weight: bold;
+}
+
+QLabel#placeholderDesc {
+ color: #3a4055;
+ font-size: 13px;
+}
+
+QLabel#placeholderMsg {
+ color: #333841;
+ font-size: 12px;
+}
+
+/* ─── BARRA DE ESTADO ───────────────────────────────────────── */
QStatusBar {
- background-color: #1565c0;
+ background-color: #0d99ff;
color: #ffffff;
font-size: 12px;
}
QStatusBar::item { border: none; }
-/* Panel Hidrostáticos — siempre visible abajo */
-#hydrostaticsPanel {
- background-color: #151a22;
- border-top: 2px solid #1976d2;
+QLabel#statusVersion {
+ color: rgba(255,255,255,0.85);
+ font-size: 11px;
+ font-weight: bold;
+}
+QLabel#statusLabel {
+ color: rgba(255,255,255,0.70);
+ font-size: 11px;
+}
+QLabel#statusValue {
+ color: #ffffff;
+ font-size: 11px;
+ font-weight: bold;
}
-#hydrostaticsPanel QLabel { font-family: "Consolas", monospace; font-size: 12px; }
-#hydrostaticsPanel QLabel[value="true"] { color: #80deea; font-weight: bold; }
-#hydrostaticsPanel QLabel[label="true"] { color: #7a8090; }
-#hydrostaticsPanel QLabel[imo_ok="true"] { color: #66bb6a; font-weight: bold; }
-#hydrostaticsPanel QLabel[imo_fail="true"] { color: #ef5350; font-weight: bold; }
-/* Panel Árbol de Proyecto */
-#projectTree { border-right: 1px solid #3a3f4b; }
+/* ─── SCROLLBARS ────────────────────────────────────────────── */
-/* Panel Propiedades */
-#propertiesPanel { border-left: 1px solid #3a3f4b; }
+QScrollBar:vertical {
+ background: #1e2128;
+ width: 10px;
+ border: none;
+}
+QScrollBar::handle:vertical {
+ background: #3a3f4b;
+ border-radius: 5px;
+ min-height: 24px;
+}
+QScrollBar::handle:vertical:hover { background: #555b67; }
+QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
+
+QScrollBar:horizontal {
+ background: #1e2128;
+ height: 10px;
+ border: none;
+}
+QScrollBar::handle:horizontal {
+ background: #3a3f4b;
+ border-radius: 5px;
+ min-width: 24px;
+}
+QScrollBar::handle:horizontal:hover { background: #555b67; }
+QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }
+
+/* ─── BOTONES ───────────────────────────────────────────────── */
+
+QPushButton {
+ background-color: #2d3139;
+ color: #e8eaf0;
+ border: 1px solid #3a3f4b;
+ border-radius: 3px;
+ padding: 5px 14px;
+ min-width: 72px;
+}
+QPushButton:hover {
+ background-color: #3a3f4b;
+ border-color: #555b67;
+}
+QPushButton:pressed {
+ background-color: #0d99ff;
+ color: #ffffff;
+ border-color: #0d99ff;
+}
+QPushButton:default { border-color: #0d99ff; }
+QPushButton:disabled { color: #555b67; border-color: #2d3139; }
+
+/* ─── CAMPOS DE ENTRADA ─────────────────────────────────────── */
+
+QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {
+ background-color: #252830;
+ color: #e8eaf0;
+ border: 1px solid #3a3f4b;
+ border-radius: 3px;
+ padding: 4px 6px;
+ selection-background-color: #0d99ff;
+}
+QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {
+ border-color: #0d99ff;
+}
+QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled {
+ color: #555b67;
+ background-color: #1e2128;
+}
+QComboBox::drop-down { border: none; width: 20px; }
+QComboBox QAbstractItemView {
+ background-color: #252830;
+ color: #e8eaf0;
+ border: 1px solid #3a3f4b;
+ selection-background-color: #0d99ff;
+}
+
+/* ─── TABLAS Y LISTAS ───────────────────────────────────────── */
+
+QHeaderView::section {
+ background-color: #252830;
+ color: #8b919e;
+ border: none;
+ border-right: 1px solid #333841;
+ border-bottom: 1px solid #333841;
+ padding: 4px 8px;
+ font-size: 12px;
+}
+QTableView, QListView {
+ background-color: #1e2128;
+ alternate-background-color: #212630;
+ color: #e8eaf0;
+ border: 1px solid #333841;
+ gridline-color: #2d3139;
+ selection-background-color: #0d99ff;
+ selection-color: #ffffff;
+}
+QTableView::item:hover, QListView::item:hover {
+ background-color: #2d3139;
+}
+
+/* ─── ÁRBOL ─────────────────────────────────────────────────── */
+
+QTreeWidget {
+ background-color: #1e2128;
+ alternate-background-color: #212630;
+ color: #e8eaf0;
+ border: none;
+ selection-background-color: #0d99ff;
+ selection-color: #ffffff;
+ font-size: 13px;
+}
+QTreeWidget::item {
+ padding: 3px 4px;
+ border: none;
+}
+QTreeWidget::item:hover {
+ background-color: #2d3139;
+}
+QTreeWidget::item:selected {
+ background-color: #0d99ff;
+ color: #ffffff;
+}
+QTreeWidget::branch {
+ background-color: #1e2128;
+}
+
+/* ─── PESTAÑAS ──────────────────────────────────────────────── */
+
+QTabWidget::pane {
+ border: none;
+ border-top: 1px solid #333841;
+ background-color: #1e2128;
+}
+QTabBar {
+ background-color: #252830;
+}
+QTabBar::tab {
+ background-color: #252830;
+ color: #8b919e;
+ border: none;
+ border-right: 1px solid #333841;
+ padding: 6px 14px;
+ min-width: 70px;
+ font-size: 12px;
+}
+QTabBar::tab:selected {
+ background-color: #1e2128;
+ color: #e8eaf0;
+ border-top: 2px solid #0d99ff;
+}
+QTabBar::tab:hover:!selected {
+ background-color: #2d3139;
+ color: #e8eaf0;
+}
+
+/* ─── SPLITTER GENERAL ──────────────────────────────────────── */
+
+QSplitter::handle { background-color: #333841; }
+QSplitter::handle:horizontal { width: 1px; }
+QSplitter::handle:vertical { height: 1px; }
+
+/* ─── MENSAJES / TOOLTIP ────────────────────────────────────── */
+
+QMessageBox { background-color: #252830; }
+QMessageBox QLabel { color: #e8eaf0; }
+
+QToolTip {
+ background-color: #252830;
+ color: #e8eaf0;
+ border: 1px solid #3a3f4b;
+ padding: 4px 6px;
+ font-size: 12px;
+}
+
+/* ─── WIDGET DE INFO (ribbonInfoWidget legado) ──────────────── */
+
+QWidget#ribbonInfoWidget {
+ background-color: transparent;
+ border-left: 1px solid #333841;
+ padding-left: 4px;
+}
+QLabel#tbLabel {
+ color: #8b919e;
+ font-size: 11px;
+}
+QLabel#tbValue {
+ color: #0d99ff;
+ font-weight: bold;
+ font-size: 11px;
+}