gui.panels.log_panel
1import subprocess 2from pathlib import Path 3 4from PySide6.QtWidgets import ( 5 QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit, 6 QPushButton, QLabel, QScrollArea, QProgressBar, 7) 8from PySide6.QtCore import Signal, Qt 9from PySide6.QtGui import QFont, QTextCursor 10 11from gui.theme import c, signal as theme_signal 12from lib.i18n import _ 13 14 15class LogPanel(QWidget): 16 collapse_toggled = Signal(bool) # True = pide colapsar, False = pide expandir 17 18 def __init__(self, parent=None): 19 super().__init__(parent) 20 self._collapsed = False 21 self._odp_dir: Path | None = None 22 self._build_ui() 23 theme_signal().changed.connect(self._on_theme_changed) 24 25 def _build_ui(self): 26 layout = QVBoxLayout(self) 27 layout.setContentsMargins(0, 0, 0, 0) 28 layout.setSpacing(0) 29 30 layout.addWidget(self._make_header()) 31 32 self._progress_bar = QProgressBar() 33 self._progress_bar.setObjectName('build_progress') 34 self._progress_bar.setFixedHeight(4) 35 self._progress_bar.setTextVisible(False) 36 self._progress_bar.setRange(0, 100) 37 self._progress_bar.setValue(0) 38 self._progress_bar.setVisible(False) 39 layout.addWidget(self._progress_bar) 40 41 self.text_area = QPlainTextEdit() 42 self.text_area.setObjectName('log_text') 43 self.text_area.setReadOnly(True) 44 self.text_area.setFont(QFont('Liberation Mono', 10)) 45 layout.addWidget(self.text_area) 46 47 self._results_panel = self._make_results_panel() 48 self._results_panel.setVisible(False) 49 layout.addWidget(self._results_panel) 50 51 self._refresh_log_colors() 52 53 # ── Header ──────────────────────────────────────────────────────────────── 54 55 def _make_header(self) -> QWidget: 56 header = QWidget() 57 header.setObjectName('log_header') 58 header.setFixedHeight(36) 59 hlay = QHBoxLayout(header) 60 hlay.setContentsMargins(12, 0, 8, 0) 61 hlay.setSpacing(4) 62 63 self._collapse_btn = QPushButton('▼') 64 self._collapse_btn.setObjectName('log_collapse_btn') 65 self._collapse_btn.setToolTip(_('Colapsar consola')) 66 self._collapse_btn.setFixedSize(24, 24) 67 self._collapse_btn.clicked.connect(self._toggle_collapse) 68 hlay.addWidget(self._collapse_btn) 69 70 lbl = QLabel(_('Consola')) 71 lbl.setObjectName('log_title') 72 hlay.addWidget(lbl) 73 hlay.addStretch() 74 75 for label, slot in ((_('Limpiar'), self.clear_log), (_('Copiar'), self._copy)): 76 btn = QPushButton(label) 77 btn.setObjectName('log_btn') 78 btn.setFixedHeight(24) 79 btn.clicked.connect(slot) 80 hlay.addWidget(btn) 81 82 return header 83 84 # ── Panel de resultados (tabla de ficheros generados) ───────────────────── 85 86 def _make_results_panel(self) -> QWidget: 87 panel = QWidget() 88 panel.setObjectName('results_bar') 89 vlay = QVBoxLayout(panel) 90 vlay.setContentsMargins(0, 0, 0, 0) 91 vlay.setSpacing(0) 92 93 # Cabecera: carpeta + cerrar 94 hdr = QWidget() 95 hdr.setObjectName('results_bar') 96 hdr.setFixedHeight(36) 97 hlay = QHBoxLayout(hdr) 98 hlay.setContentsMargins(12, 6, 8, 6) 99 hlay.setSpacing(8) 100 101 lbl = QLabel(_('Generados:')) 102 lbl.setObjectName('results_label') 103 hlay.addWidget(lbl) 104 105 self._btn_folder = QPushButton(_('📁 Carpeta')) 106 self._btn_folder.setObjectName('open_result_btn') 107 self._btn_folder.setToolTip(_('Abrir carpeta de salida')) 108 self._btn_folder.clicked.connect(self._open_folder) 109 hlay.addWidget(self._btn_folder) 110 111 hlay.addStretch() 112 113 btn_hide = QPushButton('✕') 114 btn_hide.setObjectName('log_btn') 115 btn_hide.setFixedSize(24, 24) 116 btn_hide.setToolTip(_('Ocultar')) 117 btn_hide.clicked.connect(lambda: self._results_panel.setVisible(False)) 118 hlay.addWidget(btn_hide) 119 120 vlay.addWidget(hdr) 121 122 # Área scrollable con las filas de presentaciones 123 self._scroll_area = QScrollArea() 124 self._scroll_area.setWidgetResizable(True) 125 self._scroll_area.setHorizontalScrollBarPolicy( 126 Qt.ScrollBarPolicy.ScrollBarAlwaysOff 127 ) 128 self._scroll_area.setMaximumHeight(160) 129 self._scroll_area.setFrameShape(QScrollArea.Shape.NoFrame) 130 self._scroll_area.setObjectName('results_scroll') 131 132 self._rows_widget = QWidget() 133 self._rows_widget.setObjectName('results_bar') 134 self._rows_layout = QVBoxLayout(self._rows_widget) 135 self._rows_layout.setContentsMargins(0, 2, 0, 4) 136 self._rows_layout.setSpacing(1) 137 self._rows_layout.addStretch() 138 139 self._scroll_area.setWidget(self._rows_widget) 140 vlay.addWidget(self._scroll_area) 141 142 return panel 143 144 def _clear_result_rows(self) -> None: 145 while self._rows_layout.count() > 1: # deja el stretch final 146 item = self._rows_layout.takeAt(0) 147 if item.widget(): 148 item.widget().deleteLater() 149 150 def _add_result_row(self, odp: Path, pdf: Path | None) -> None: 151 row = QWidget() 152 row.setObjectName('results_bar') 153 row.setFixedHeight(30) 154 hlay = QHBoxLayout(row) 155 hlay.setContentsMargins(12, 2, 8, 2) 156 hlay.setSpacing(6) 157 158 # Nombre del fichero (sin versión para legibilidad) 159 name_lbl = QLabel(odp.name) 160 name_lbl.setObjectName('results_label') 161 name_lbl.setToolTip(str(odp)) 162 hlay.addWidget(name_lbl, 1) 163 164 btn_odp = QPushButton(_('ODP')) 165 btn_odp.setObjectName('open_result_btn') 166 btn_odp.setFixedHeight(22) 167 btn_odp.clicked.connect(lambda _, p=odp: subprocess.Popen(['xdg-open', str(p)])) 168 hlay.addWidget(btn_odp) 169 170 if pdf and pdf.exists(): 171 btn_pdf = QPushButton(_('PDF')) 172 btn_pdf.setObjectName('open_result_btn') 173 btn_pdf.setFixedHeight(22) 174 btn_pdf.clicked.connect(lambda _, p=pdf: subprocess.Popen(['xdg-open', str(p)])) 175 hlay.addWidget(btn_pdf) 176 177 # Insertar antes del stretch 178 self._rows_layout.insertWidget(self._rows_layout.count() - 1, row) 179 180 # ── Colapsar/expandir ───────────────────────────────────────────────────── 181 182 def _toggle_collapse(self) -> None: 183 self._collapsed = not self._collapsed 184 if self._collapsed: 185 self._collapse_btn.setText('▲') 186 self._collapse_btn.setToolTip(_('Expandir consola')) 187 self.text_area.hide() 188 self._results_panel.hide() 189 self.setMaximumHeight(36) 190 else: 191 self._collapse_btn.setText('▼') 192 self._collapse_btn.setToolTip(_('Colapsar consola')) 193 self.text_area.show() 194 self.setMaximumHeight(16777215) 195 self.collapse_toggled.emit(self._collapsed) 196 197 # ── API pública ─────────────────────────────────────────────────────────── 198 199 def set_progress(self, current: int, total: int) -> None: 200 if total <= 0: 201 self._progress_bar.setVisible(False) 202 return 203 pct = int(current * 100 / total) 204 self._progress_bar.setValue(pct) 205 self._progress_bar.setVisible(pct < 100) 206 207 def show_build_results(self, files: list) -> None: 208 """Muestra los ficheros generados. files: list[tuple[Path, Path | None]].""" 209 if not files: 210 self._results_panel.setVisible(False) 211 return 212 213 self._odp_dir = files[0][0].parent 214 self._clear_result_rows() 215 for odp, pdf in files: 216 self._add_result_row(odp, pdf) 217 218 row_h = 30 219 content_h = len(files) * (row_h + 1) + 8 220 self._scroll_area.setMaximumHeight(min(content_h, 160)) 221 self._results_panel.setVisible(True) 222 223 def hide_open_buttons(self) -> None: 224 self._results_panel.setVisible(False) 225 self._progress_bar.setValue(0) 226 self._progress_bar.setVisible(False) 227 228 def append_line(self, text: str, level: str = 'info') -> None: 229 color = self._colors.get(level, self._colors['info']) 230 self.text_area.appendHtml( 231 f'<span style="color:{color};white-space:pre;">{_escape(text)}</span>' 232 ) 233 self.text_area.moveCursor(QTextCursor.MoveOperation.End) 234 235 def clear_log(self) -> None: 236 self.text_area.clear() 237 238 def toPlainText(self) -> str: 239 return self.text_area.toPlainText() 240 241 # ── Internos ────────────────────────────────────────────────────────────── 242 243 def _open_folder(self) -> None: 244 if self._odp_dir and self._odp_dir.exists(): 245 subprocess.Popen(['xdg-open', str(self._odp_dir)]) 246 247 def _copy(self) -> None: 248 from PySide6.QtWidgets import QApplication 249 QApplication.clipboard().setText(self.text_area.toPlainText()) 250 251 def _refresh_log_colors(self) -> None: 252 self._colors = { 253 'ok': c('log_ok'), 254 'error': c('log_error'), 255 'info': c('log_info'), 256 } 257 258 def _on_theme_changed(self) -> None: 259 self._refresh_log_colors() 260 261 262def _escape(text: str) -> str: 263 return text.replace('&', '&').replace('<', '<').replace('>', '>')
16class LogPanel(QWidget): 17 collapse_toggled = Signal(bool) # True = pide colapsar, False = pide expandir 18 19 def __init__(self, parent=None): 20 super().__init__(parent) 21 self._collapsed = False 22 self._odp_dir: Path | None = None 23 self._build_ui() 24 theme_signal().changed.connect(self._on_theme_changed) 25 26 def _build_ui(self): 27 layout = QVBoxLayout(self) 28 layout.setContentsMargins(0, 0, 0, 0) 29 layout.setSpacing(0) 30 31 layout.addWidget(self._make_header()) 32 33 self._progress_bar = QProgressBar() 34 self._progress_bar.setObjectName('build_progress') 35 self._progress_bar.setFixedHeight(4) 36 self._progress_bar.setTextVisible(False) 37 self._progress_bar.setRange(0, 100) 38 self._progress_bar.setValue(0) 39 self._progress_bar.setVisible(False) 40 layout.addWidget(self._progress_bar) 41 42 self.text_area = QPlainTextEdit() 43 self.text_area.setObjectName('log_text') 44 self.text_area.setReadOnly(True) 45 self.text_area.setFont(QFont('Liberation Mono', 10)) 46 layout.addWidget(self.text_area) 47 48 self._results_panel = self._make_results_panel() 49 self._results_panel.setVisible(False) 50 layout.addWidget(self._results_panel) 51 52 self._refresh_log_colors() 53 54 # ── Header ──────────────────────────────────────────────────────────────── 55 56 def _make_header(self) -> QWidget: 57 header = QWidget() 58 header.setObjectName('log_header') 59 header.setFixedHeight(36) 60 hlay = QHBoxLayout(header) 61 hlay.setContentsMargins(12, 0, 8, 0) 62 hlay.setSpacing(4) 63 64 self._collapse_btn = QPushButton('▼') 65 self._collapse_btn.setObjectName('log_collapse_btn') 66 self._collapse_btn.setToolTip(_('Colapsar consola')) 67 self._collapse_btn.setFixedSize(24, 24) 68 self._collapse_btn.clicked.connect(self._toggle_collapse) 69 hlay.addWidget(self._collapse_btn) 70 71 lbl = QLabel(_('Consola')) 72 lbl.setObjectName('log_title') 73 hlay.addWidget(lbl) 74 hlay.addStretch() 75 76 for label, slot in ((_('Limpiar'), self.clear_log), (_('Copiar'), self._copy)): 77 btn = QPushButton(label) 78 btn.setObjectName('log_btn') 79 btn.setFixedHeight(24) 80 btn.clicked.connect(slot) 81 hlay.addWidget(btn) 82 83 return header 84 85 # ── Panel de resultados (tabla de ficheros generados) ───────────────────── 86 87 def _make_results_panel(self) -> QWidget: 88 panel = QWidget() 89 panel.setObjectName('results_bar') 90 vlay = QVBoxLayout(panel) 91 vlay.setContentsMargins(0, 0, 0, 0) 92 vlay.setSpacing(0) 93 94 # Cabecera: carpeta + cerrar 95 hdr = QWidget() 96 hdr.setObjectName('results_bar') 97 hdr.setFixedHeight(36) 98 hlay = QHBoxLayout(hdr) 99 hlay.setContentsMargins(12, 6, 8, 6) 100 hlay.setSpacing(8) 101 102 lbl = QLabel(_('Generados:')) 103 lbl.setObjectName('results_label') 104 hlay.addWidget(lbl) 105 106 self._btn_folder = QPushButton(_('📁 Carpeta')) 107 self._btn_folder.setObjectName('open_result_btn') 108 self._btn_folder.setToolTip(_('Abrir carpeta de salida')) 109 self._btn_folder.clicked.connect(self._open_folder) 110 hlay.addWidget(self._btn_folder) 111 112 hlay.addStretch() 113 114 btn_hide = QPushButton('✕') 115 btn_hide.setObjectName('log_btn') 116 btn_hide.setFixedSize(24, 24) 117 btn_hide.setToolTip(_('Ocultar')) 118 btn_hide.clicked.connect(lambda: self._results_panel.setVisible(False)) 119 hlay.addWidget(btn_hide) 120 121 vlay.addWidget(hdr) 122 123 # Área scrollable con las filas de presentaciones 124 self._scroll_area = QScrollArea() 125 self._scroll_area.setWidgetResizable(True) 126 self._scroll_area.setHorizontalScrollBarPolicy( 127 Qt.ScrollBarPolicy.ScrollBarAlwaysOff 128 ) 129 self._scroll_area.setMaximumHeight(160) 130 self._scroll_area.setFrameShape(QScrollArea.Shape.NoFrame) 131 self._scroll_area.setObjectName('results_scroll') 132 133 self._rows_widget = QWidget() 134 self._rows_widget.setObjectName('results_bar') 135 self._rows_layout = QVBoxLayout(self._rows_widget) 136 self._rows_layout.setContentsMargins(0, 2, 0, 4) 137 self._rows_layout.setSpacing(1) 138 self._rows_layout.addStretch() 139 140 self._scroll_area.setWidget(self._rows_widget) 141 vlay.addWidget(self._scroll_area) 142 143 return panel 144 145 def _clear_result_rows(self) -> None: 146 while self._rows_layout.count() > 1: # deja el stretch final 147 item = self._rows_layout.takeAt(0) 148 if item.widget(): 149 item.widget().deleteLater() 150 151 def _add_result_row(self, odp: Path, pdf: Path | None) -> None: 152 row = QWidget() 153 row.setObjectName('results_bar') 154 row.setFixedHeight(30) 155 hlay = QHBoxLayout(row) 156 hlay.setContentsMargins(12, 2, 8, 2) 157 hlay.setSpacing(6) 158 159 # Nombre del fichero (sin versión para legibilidad) 160 name_lbl = QLabel(odp.name) 161 name_lbl.setObjectName('results_label') 162 name_lbl.setToolTip(str(odp)) 163 hlay.addWidget(name_lbl, 1) 164 165 btn_odp = QPushButton(_('ODP')) 166 btn_odp.setObjectName('open_result_btn') 167 btn_odp.setFixedHeight(22) 168 btn_odp.clicked.connect(lambda _, p=odp: subprocess.Popen(['xdg-open', str(p)])) 169 hlay.addWidget(btn_odp) 170 171 if pdf and pdf.exists(): 172 btn_pdf = QPushButton(_('PDF')) 173 btn_pdf.setObjectName('open_result_btn') 174 btn_pdf.setFixedHeight(22) 175 btn_pdf.clicked.connect(lambda _, p=pdf: subprocess.Popen(['xdg-open', str(p)])) 176 hlay.addWidget(btn_pdf) 177 178 # Insertar antes del stretch 179 self._rows_layout.insertWidget(self._rows_layout.count() - 1, row) 180 181 # ── Colapsar/expandir ───────────────────────────────────────────────────── 182 183 def _toggle_collapse(self) -> None: 184 self._collapsed = not self._collapsed 185 if self._collapsed: 186 self._collapse_btn.setText('▲') 187 self._collapse_btn.setToolTip(_('Expandir consola')) 188 self.text_area.hide() 189 self._results_panel.hide() 190 self.setMaximumHeight(36) 191 else: 192 self._collapse_btn.setText('▼') 193 self._collapse_btn.setToolTip(_('Colapsar consola')) 194 self.text_area.show() 195 self.setMaximumHeight(16777215) 196 self.collapse_toggled.emit(self._collapsed) 197 198 # ── API pública ─────────────────────────────────────────────────────────── 199 200 def set_progress(self, current: int, total: int) -> None: 201 if total <= 0: 202 self._progress_bar.setVisible(False) 203 return 204 pct = int(current * 100 / total) 205 self._progress_bar.setValue(pct) 206 self._progress_bar.setVisible(pct < 100) 207 208 def show_build_results(self, files: list) -> None: 209 """Muestra los ficheros generados. files: list[tuple[Path, Path | None]].""" 210 if not files: 211 self._results_panel.setVisible(False) 212 return 213 214 self._odp_dir = files[0][0].parent 215 self._clear_result_rows() 216 for odp, pdf in files: 217 self._add_result_row(odp, pdf) 218 219 row_h = 30 220 content_h = len(files) * (row_h + 1) + 8 221 self._scroll_area.setMaximumHeight(min(content_h, 160)) 222 self._results_panel.setVisible(True) 223 224 def hide_open_buttons(self) -> None: 225 self._results_panel.setVisible(False) 226 self._progress_bar.setValue(0) 227 self._progress_bar.setVisible(False) 228 229 def append_line(self, text: str, level: str = 'info') -> None: 230 color = self._colors.get(level, self._colors['info']) 231 self.text_area.appendHtml( 232 f'<span style="color:{color};white-space:pre;">{_escape(text)}</span>' 233 ) 234 self.text_area.moveCursor(QTextCursor.MoveOperation.End) 235 236 def clear_log(self) -> None: 237 self.text_area.clear() 238 239 def toPlainText(self) -> str: 240 return self.text_area.toPlainText() 241 242 # ── Internos ────────────────────────────────────────────────────────────── 243 244 def _open_folder(self) -> None: 245 if self._odp_dir and self._odp_dir.exists(): 246 subprocess.Popen(['xdg-open', str(self._odp_dir)]) 247 248 def _copy(self) -> None: 249 from PySide6.QtWidgets import QApplication 250 QApplication.clipboard().setText(self.text_area.toPlainText()) 251 252 def _refresh_log_colors(self) -> None: 253 self._colors = { 254 'ok': c('log_ok'), 255 'error': c('log_error'), 256 'info': c('log_info'), 257 } 258 259 def _on_theme_changed(self) -> None: 260 self._refresh_log_colors()
QWidget(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, modal: bool | None = None, windowModality: PySide6.QtCore.Qt.WindowModality | None = None, enabled: bool | None = None, geometry: PySide6.QtCore.QRect | None = None, frameGeometry: PySide6.QtCore.QRect | None = None, normalGeometry: PySide6.QtCore.QRect | None = None, x: int | None = None, y: int | None = None, pos: PySide6.QtCore.QPoint | None = None, frameSize: PySide6.QtCore.QSize | None = None, size: PySide6.QtCore.QSize | None = None, width: int | None = None, height: int | None = None, rect: PySide6.QtCore.QRect | None = None, childrenRect: PySide6.QtCore.QRect | None = None, childrenRegion: PySide6.QtGui.QRegion | None = None, sizePolicy: PySide6.QtWidgets.QSizePolicy | None = None, minimumSize: PySide6.QtCore.QSize | None = None, maximumSize: PySide6.QtCore.QSize | None = None, minimumWidth: int | None = None, minimumHeight: int | None = None, maximumWidth: int | None = None, maximumHeight: int | None = None, sizeIncrement: PySide6.QtCore.QSize | None = None, baseSize: PySide6.QtCore.QSize | None = None, palette: PySide6.QtGui.QPalette | None = None, font: PySide6.QtGui.QFont | None = None, cursor: PySide6.QtGui.QCursor | None = None, mouseTracking: bool | None = None, tabletTracking: bool | None = None, isActiveWindow: bool | None = None, focusPolicy: PySide6.QtCore.Qt.FocusPolicy | None = None, focus: bool | None = None, contextMenuPolicy: PySide6.QtCore.Qt.ContextMenuPolicy | None = None, updatesEnabled: bool | None = None, visible: bool | None = None, minimized: bool | None = None, maximized: bool | None = None, fullScreen: bool | None = None, sizeHint: PySide6.QtCore.QSize | None = None, minimumSizeHint: PySide6.QtCore.QSize | None = None, acceptDrops: bool | None = None, windowTitle: str | None = None, windowIcon: PySide6.QtGui.QIcon | None = None, windowIconText: str | None = None, windowOpacity: float | None = None, windowModified: bool | None = None, toolTip: str | None = None, toolTipDuration: int | None = None, statusTip: str | None = None, whatsThis: str | None = None, accessibleName: str | None = None, accessibleDescription: str | None = None, accessibleIdentifier: str | None = None, layoutDirection: PySide6.QtCore.Qt.LayoutDirection | None = None, autoFillBackground: bool | None = None, styleSheet: str | None = None, locale: PySide6.QtCore.QLocale | None = None, windowFilePath: str | None = None, inputMethodHints: PySide6.QtCore.Qt.InputMethodHint | None = None) -> None
208 def show_build_results(self, files: list) -> None: 209 """Muestra los ficheros generados. files: list[tuple[Path, Path | None]].""" 210 if not files: 211 self._results_panel.setVisible(False) 212 return 213 214 self._odp_dir = files[0][0].parent 215 self._clear_result_rows() 216 for odp, pdf in files: 217 self._add_result_row(odp, pdf) 218 219 row_h = 30 220 content_h = len(files) * (row_h + 1) + 8 221 self._scroll_area.setMaximumHeight(min(content_h, 160)) 222 self._results_panel.setVisible(True)
Muestra los ficheros generados. files: list[tuple[Path, Path | None]].