plano3d/app/ventana.py

158 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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)