"""Ventana principal de Plano3D: abrir un plano y mostrar el resultado del motor.""" from __future__ import annotations from pathlib import Path from PySide6.QtCore import QMimeData, Qt from PySide6.QtGui import QAction, QDragEnterEvent, QDropEvent, QKeySequence from PySide6.QtWidgets import ( QFileDialog, QLabel, QListWidget, QListWidgetItem, QMainWindow, QMessageBox, QStackedWidget, QStyle, QVBoxLayout, QWidget, ) from app.procesado import HiloProcesado from motor.api import Estado, Gravedad, Resultado EXTENSIONES = {".dxf"} # se ampliará con .dwg y .pdf cuando el motor los admita TEXTO_ESTADO = { Estado.OK: "Pieza reconstruida", Estado.CON_AVISOS: "Pieza reconstruida con avisos", Estado.IMPOSIBLE: "No se ha podido reconstruir la pieza", } ICONO_GRAVEDAD = { Gravedad.INFO: QStyle.StandardPixmap.SP_MessageBoxInformation, Gravedad.AVISO: QStyle.StandardPixmap.SP_MessageBoxWarning, Gravedad.BLOQUEANTE: QStyle.StandardPixmap.SP_MessageBoxCritical, } class VentanaPrincipal(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Plano3D") self.resize(1000, 700) self.setAcceptDrops(True) self.resultado: Resultado | None = None self.ruta_actual: Path | None = None self._hilo: HiloProcesado | None = None self._crear_menu() self._crear_contenido() self.statusBar().showMessage("Listo") # ------------------------------------------------------------------ construcción def _crear_menu(self) -> None: self.accion_abrir = QAction("&Abrir plano…", self) self.accion_abrir.setShortcut(QKeySequence.StandardKey.Open) self.accion_abrir.triggered.connect(self.elegir_plano) accion_salir = QAction("&Salir", self) accion_salir.setShortcut(QKeySequence.StandardKey.Quit) accion_salir.triggered.connect(self.close) menu_archivo = self.menuBar().addMenu("&Archivo") menu_archivo.addAction(self.accion_abrir) menu_archivo.addSeparator() menu_archivo.addAction(accion_salir) def _crear_contenido(self) -> None: # Página 0: indicación inicial. Página 1: resultado del plano. self.paginas = QStackedWidget() self.etiqueta_inicio = QLabel("Arrastra un plano DXF a esta ventana\no usa Archivo › Abrir plano (Ctrl+O)") self.etiqueta_inicio.setAlignment(Qt.AlignmentFlag.AlignCenter) self.paginas.addWidget(self.etiqueta_inicio) pagina_resultado = QWidget() disposicion = QVBoxLayout(pagina_resultado) self.etiqueta_archivo = QLabel() self.etiqueta_estado = QLabel() fuente = self.etiqueta_estado.font() fuente.setPointSize(fuente.pointSize() + 2) fuente.setBold(True) self.etiqueta_estado.setFont(fuente) self.lista_avisos = QListWidget() self.lista_avisos.setWordWrap(True) # los avisos largos se parten en varias líneas self.lista_avisos.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) disposicion.addWidget(self.etiqueta_archivo) disposicion.addWidget(self.etiqueta_estado) disposicion.addWidget(QLabel("Informe del plano:")) disposicion.addWidget(self.lista_avisos) self.paginas.addWidget(pagina_resultado) self.setCentralWidget(self.paginas) # ------------------------------------------------------------------ abrir planos def elegir_plano(self) -> None: ruta, _ = QFileDialog.getOpenFileName(self, "Abrir plano", "", "Planos DXF (*.dxf)") if ruta: self.cargar_plano(Path(ruta)) def cargar_plano(self, ruta: Path) -> None: """Lanza el procesado de un plano. El resultado llega por _mostrar_resultado.""" if self._hilo is not None and self._hilo.isRunning(): return # ya hay un plano en proceso if ruta.suffix.lower() not in EXTENSIONES: QMessageBox.warning(self, "Formato no admitido", f"No se puede abrir «{ruta.name}»: de momento solo se admiten planos DXF.") return self.ruta_actual = ruta self.accion_abrir.setEnabled(False) self.statusBar().showMessage(f"Procesando {ruta.name}…") self._hilo = HiloProcesado(ruta, parent=self) self._hilo.terminado.connect(self._mostrar_resultado) self._hilo.fallido.connect(self._mostrar_error) self._hilo.finished.connect(lambda: self.accion_abrir.setEnabled(True)) self._hilo.start() # Arrastrar y soltar: se acepta solo si se arrastra un único archivo con extensión admitida. def dragEnterEvent(self, evento: QDragEnterEvent) -> None: if self._ruta_arrastrada(evento.mimeData()) is not None: evento.acceptProposedAction() def dropEvent(self, evento: QDropEvent) -> None: ruta = self._ruta_arrastrada(evento.mimeData()) if ruta is not None: self.cargar_plano(ruta) @staticmethod def _ruta_arrastrada(datos: QMimeData) -> Path | None: urls = datos.urls() if len(urls) != 1 or not urls[0].isLocalFile(): return None ruta = Path(urls[0].toLocalFile()) return ruta if ruta.suffix.lower() in EXTENSIONES else None # ------------------------------------------------------------------ resultado def _mostrar_resultado(self, resultado: Resultado) -> None: self.resultado = resultado self.etiqueta_archivo.setText(f"Plano: {self.ruta_actual.name} · Escala aplicada: ×{resultado.escala:g}") self.etiqueta_estado.setText(TEXTO_ESTADO[resultado.estado]) self.lista_avisos.clear() for aviso in resultado.avisos: elemento = QListWidgetItem(self.style().standardIcon(ICONO_GRAVEDAD[aviso.gravedad]), aviso.mensaje) elemento.setToolTip(aviso.codigo) self.lista_avisos.addItem(elemento) if not resultado.avisos: self.lista_avisos.addItem("Sin observaciones.") self.paginas.setCurrentIndex(1) self.statusBar().showMessage("Procesado terminado", 5000) def _mostrar_error(self, mensaje: str) -> None: self.statusBar().showMessage("Error al procesar el plano", 5000) QMessageBox.critical(self, "Error al procesar el plano", mensaje)