gui.panels.log_panel
1# Copyright (C) 2026 David Vaquero <pepesan@gmail.com> 2# 3# This program is free software: you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation, either version 3 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program. If not, see <https://www.gnu.org/licenses/>. 15 16import subprocess 17from pathlib import Path 18 19from PySide6.QtWidgets import ( 20 QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit, 21 QPushButton, QLabel, QScrollArea, QProgressBar, 22) 23from PySide6.QtCore import Signal, Qt 24from PySide6.QtGui import QFont, QTextCursor 25 26from gui.theme import c, signal as theme_signal 27from lib.i18n import _ 28 29 30class LogPanel(QWidget): 31 collapse_toggled = Signal(bool) # True = pide colapsar, False = pide expandir 32 33 def __init__(self, parent=None): 34 super().__init__(parent) 35 self._collapsed = False 36 self._odp_dir: Path | None = None 37 self._build_ui() 38 theme_signal().changed.connect(self._on_theme_changed) 39 40 def _build_ui(self): 41 layout = QVBoxLayout(self) 42 layout.setContentsMargins(0, 0, 0, 0) 43 layout.setSpacing(0) 44 45 layout.addWidget(self._make_header()) 46 47 self._progress_bar = QProgressBar() 48 self._progress_bar.setObjectName('build_progress') 49 self._progress_bar.setFixedHeight(4) 50 self._progress_bar.setTextVisible(False) 51 self._progress_bar.setRange(0, 100) 52 self._progress_bar.setValue(0) 53 self._progress_bar.setVisible(False) 54 layout.addWidget(self._progress_bar) 55 56 self.text_area = QPlainTextEdit() 57 self.text_area.setObjectName('log_text') 58 self.text_area.setReadOnly(True) 59 self.text_area.setFont(QFont('Liberation Mono', 10)) 60 layout.addWidget(self.text_area) 61 62 self._results_panel = self._make_results_panel() 63 self._results_panel.setVisible(False) 64 layout.addWidget(self._results_panel) 65 66 self._refresh_log_colors() 67 68 # ── Header ──────────────────────────────────────────────────────────────── 69 70 def _make_header(self) -> QWidget: 71 header = QWidget() 72 header.setObjectName('log_header') 73 header.setFixedHeight(36) 74 hlay = QHBoxLayout(header) 75 hlay.setContentsMargins(12, 0, 8, 0) 76 hlay.setSpacing(4) 77 78 self._collapse_btn = QPushButton('▼') 79 self._collapse_btn.setObjectName('log_collapse_btn') 80 self._collapse_btn.setToolTip(_('Colapsar consola')) 81 self._collapse_btn.setFixedSize(24, 24) 82 self._collapse_btn.clicked.connect(self._toggle_collapse) 83 hlay.addWidget(self._collapse_btn) 84 85 lbl = QLabel(_('Consola')) 86 lbl.setObjectName('log_title') 87 hlay.addWidget(lbl) 88 hlay.addStretch() 89 90 for label, slot in ((_('Limpiar'), self.clear_log), (_('Copiar'), self._copy)): 91 btn = QPushButton(label) 92 btn.setObjectName('log_btn') 93 btn.setFixedHeight(24) 94 btn.clicked.connect(slot) 95 hlay.addWidget(btn) 96 97 return header 98 99 # ── Panel de resultados (tabla de ficheros generados) ───────────────────── 100 101 def _make_results_panel(self) -> QWidget: 102 panel = QWidget() 103 panel.setObjectName('results_bar') 104 vlay = QVBoxLayout(panel) 105 vlay.setContentsMargins(0, 0, 0, 0) 106 vlay.setSpacing(0) 107 108 # Cabecera: carpeta + cerrar 109 hdr = QWidget() 110 hdr.setObjectName('results_bar') 111 hdr.setFixedHeight(36) 112 hlay = QHBoxLayout(hdr) 113 hlay.setContentsMargins(12, 6, 8, 6) 114 hlay.setSpacing(8) 115 116 lbl = QLabel(_('Generados:')) 117 lbl.setObjectName('results_label') 118 hlay.addWidget(lbl) 119 120 self._btn_folder = QPushButton(_('📁 Carpeta')) 121 self._btn_folder.setObjectName('open_result_btn') 122 self._btn_folder.setToolTip(_('Abrir carpeta de salida')) 123 self._btn_folder.clicked.connect(self._open_folder) 124 hlay.addWidget(self._btn_folder) 125 126 hlay.addStretch() 127 128 btn_hide = QPushButton('✕') 129 btn_hide.setObjectName('log_btn') 130 btn_hide.setFixedSize(24, 24) 131 btn_hide.setToolTip(_('Ocultar')) 132 btn_hide.clicked.connect(lambda: self._results_panel.setVisible(False)) 133 hlay.addWidget(btn_hide) 134 135 vlay.addWidget(hdr) 136 137 # Área scrollable con las filas de presentaciones 138 self._scroll_area = QScrollArea() 139 self._scroll_area.setWidgetResizable(True) 140 self._scroll_area.setHorizontalScrollBarPolicy( 141 Qt.ScrollBarPolicy.ScrollBarAlwaysOff 142 ) 143 self._scroll_area.setMaximumHeight(160) 144 self._scroll_area.setFrameShape(QScrollArea.Shape.NoFrame) 145 self._scroll_area.setObjectName('results_scroll') 146 147 self._rows_widget = QWidget() 148 self._rows_widget.setObjectName('results_bar') 149 self._rows_layout = QVBoxLayout(self._rows_widget) 150 self._rows_layout.setContentsMargins(0, 2, 0, 4) 151 self._rows_layout.setSpacing(1) 152 self._rows_layout.addStretch() 153 154 self._scroll_area.setWidget(self._rows_widget) 155 vlay.addWidget(self._scroll_area) 156 157 return panel 158 159 def _clear_result_rows(self) -> None: 160 while self._rows_layout.count() > 1: # deja el stretch final 161 item = self._rows_layout.takeAt(0) 162 if item.widget(): 163 item.widget().deleteLater() 164 165 def _add_result_row(self, odp: Path, pdf: Path | None) -> None: 166 row = QWidget() 167 row.setObjectName('results_bar') 168 row.setFixedHeight(30) 169 hlay = QHBoxLayout(row) 170 hlay.setContentsMargins(12, 2, 8, 2) 171 hlay.setSpacing(6) 172 173 # Nombre del fichero (sin versión para legibilidad) 174 name_lbl = QLabel(odp.name) 175 name_lbl.setObjectName('results_label') 176 name_lbl.setToolTip(str(odp)) 177 hlay.addWidget(name_lbl, 1) 178 179 btn_odp = QPushButton(_('ODP')) 180 btn_odp.setObjectName('open_result_btn') 181 btn_odp.setFixedHeight(22) 182 btn_odp.clicked.connect(lambda _, p=odp: subprocess.Popen(['xdg-open', str(p)])) 183 hlay.addWidget(btn_odp) 184 185 if pdf and pdf.exists(): 186 btn_pdf = QPushButton(_('PDF')) 187 btn_pdf.setObjectName('open_result_btn') 188 btn_pdf.setFixedHeight(22) 189 btn_pdf.clicked.connect(lambda _, p=pdf: subprocess.Popen(['xdg-open', str(p)])) 190 hlay.addWidget(btn_pdf) 191 192 # Insertar antes del stretch 193 self._rows_layout.insertWidget(self._rows_layout.count() - 1, row) 194 195 # ── Colapsar/expandir ───────────────────────────────────────────────────── 196 197 def _toggle_collapse(self) -> None: 198 self._collapsed = not self._collapsed 199 if self._collapsed: 200 self._collapse_btn.setText('▲') 201 self._collapse_btn.setToolTip(_('Expandir consola')) 202 self.text_area.hide() 203 self._results_panel.hide() 204 self.setMaximumHeight(36) 205 else: 206 self._collapse_btn.setText('▼') 207 self._collapse_btn.setToolTip(_('Colapsar consola')) 208 self.text_area.show() 209 self.setMaximumHeight(16777215) 210 self.collapse_toggled.emit(self._collapsed) 211 212 # ── API pública ─────────────────────────────────────────────────────────── 213 214 def set_progress(self, current: int, total: int) -> None: 215 if total <= 0: 216 self._progress_bar.setVisible(False) 217 return 218 pct = int(current * 100 / total) 219 self._progress_bar.setValue(pct) 220 self._progress_bar.setVisible(pct < 100) 221 222 def show_build_results(self, files: list) -> None: 223 """Muestra los ficheros generados. files: list[tuple[Path, Path | None]].""" 224 if not files: 225 self._results_panel.setVisible(False) 226 return 227 228 self._odp_dir = files[0][0].parent 229 self._clear_result_rows() 230 for odp, pdf in files: 231 self._add_result_row(odp, pdf) 232 233 row_h = 30 234 content_h = len(files) * (row_h + 1) + 8 235 self._scroll_area.setMaximumHeight(min(content_h, 160)) 236 self._results_panel.setVisible(True) 237 238 def hide_open_buttons(self) -> None: 239 self._results_panel.setVisible(False) 240 self._progress_bar.setValue(0) 241 self._progress_bar.setVisible(False) 242 243 def append_line(self, text: str, level: str = 'info') -> None: 244 color = self._colors.get(level, self._colors['info']) 245 self.text_area.appendHtml( 246 f'<span style="color:{color};white-space:pre;">{_escape(text)}</span>' 247 ) 248 self.text_area.moveCursor(QTextCursor.MoveOperation.End) 249 250 def clear_log(self) -> None: 251 self.text_area.clear() 252 253 def toPlainText(self) -> str: 254 return self.text_area.toPlainText() 255 256 # ── Internos ────────────────────────────────────────────────────────────── 257 258 def _open_folder(self) -> None: 259 if self._odp_dir and self._odp_dir.exists(): 260 subprocess.Popen(['xdg-open', str(self._odp_dir)]) 261 262 def _copy(self) -> None: 263 from PySide6.QtWidgets import QApplication 264 QApplication.clipboard().setText(self.text_area.toPlainText()) 265 266 def _refresh_log_colors(self) -> None: 267 self._colors = { 268 'ok': c('log_ok'), 269 'error': c('log_error'), 270 'info': c('log_info'), 271 } 272 273 def _on_theme_changed(self) -> None: 274 self._refresh_log_colors() 275 276 277def _escape(text: str) -> str: 278 return text.replace('&', '&').replace('<', '<').replace('>', '>')
31class LogPanel(QWidget): 32 collapse_toggled = Signal(bool) # True = pide colapsar, False = pide expandir 33 34 def __init__(self, parent=None): 35 super().__init__(parent) 36 self._collapsed = False 37 self._odp_dir: Path | None = None 38 self._build_ui() 39 theme_signal().changed.connect(self._on_theme_changed) 40 41 def _build_ui(self): 42 layout = QVBoxLayout(self) 43 layout.setContentsMargins(0, 0, 0, 0) 44 layout.setSpacing(0) 45 46 layout.addWidget(self._make_header()) 47 48 self._progress_bar = QProgressBar() 49 self._progress_bar.setObjectName('build_progress') 50 self._progress_bar.setFixedHeight(4) 51 self._progress_bar.setTextVisible(False) 52 self._progress_bar.setRange(0, 100) 53 self._progress_bar.setValue(0) 54 self._progress_bar.setVisible(False) 55 layout.addWidget(self._progress_bar) 56 57 self.text_area = QPlainTextEdit() 58 self.text_area.setObjectName('log_text') 59 self.text_area.setReadOnly(True) 60 self.text_area.setFont(QFont('Liberation Mono', 10)) 61 layout.addWidget(self.text_area) 62 63 self._results_panel = self._make_results_panel() 64 self._results_panel.setVisible(False) 65 layout.addWidget(self._results_panel) 66 67 self._refresh_log_colors() 68 69 # ── Header ──────────────────────────────────────────────────────────────── 70 71 def _make_header(self) -> QWidget: 72 header = QWidget() 73 header.setObjectName('log_header') 74 header.setFixedHeight(36) 75 hlay = QHBoxLayout(header) 76 hlay.setContentsMargins(12, 0, 8, 0) 77 hlay.setSpacing(4) 78 79 self._collapse_btn = QPushButton('▼') 80 self._collapse_btn.setObjectName('log_collapse_btn') 81 self._collapse_btn.setToolTip(_('Colapsar consola')) 82 self._collapse_btn.setFixedSize(24, 24) 83 self._collapse_btn.clicked.connect(self._toggle_collapse) 84 hlay.addWidget(self._collapse_btn) 85 86 lbl = QLabel(_('Consola')) 87 lbl.setObjectName('log_title') 88 hlay.addWidget(lbl) 89 hlay.addStretch() 90 91 for label, slot in ((_('Limpiar'), self.clear_log), (_('Copiar'), self._copy)): 92 btn = QPushButton(label) 93 btn.setObjectName('log_btn') 94 btn.setFixedHeight(24) 95 btn.clicked.connect(slot) 96 hlay.addWidget(btn) 97 98 return header 99 100 # ── Panel de resultados (tabla de ficheros generados) ───────────────────── 101 102 def _make_results_panel(self) -> QWidget: 103 panel = QWidget() 104 panel.setObjectName('results_bar') 105 vlay = QVBoxLayout(panel) 106 vlay.setContentsMargins(0, 0, 0, 0) 107 vlay.setSpacing(0) 108 109 # Cabecera: carpeta + cerrar 110 hdr = QWidget() 111 hdr.setObjectName('results_bar') 112 hdr.setFixedHeight(36) 113 hlay = QHBoxLayout(hdr) 114 hlay.setContentsMargins(12, 6, 8, 6) 115 hlay.setSpacing(8) 116 117 lbl = QLabel(_('Generados:')) 118 lbl.setObjectName('results_label') 119 hlay.addWidget(lbl) 120 121 self._btn_folder = QPushButton(_('📁 Carpeta')) 122 self._btn_folder.setObjectName('open_result_btn') 123 self._btn_folder.setToolTip(_('Abrir carpeta de salida')) 124 self._btn_folder.clicked.connect(self._open_folder) 125 hlay.addWidget(self._btn_folder) 126 127 hlay.addStretch() 128 129 btn_hide = QPushButton('✕') 130 btn_hide.setObjectName('log_btn') 131 btn_hide.setFixedSize(24, 24) 132 btn_hide.setToolTip(_('Ocultar')) 133 btn_hide.clicked.connect(lambda: self._results_panel.setVisible(False)) 134 hlay.addWidget(btn_hide) 135 136 vlay.addWidget(hdr) 137 138 # Área scrollable con las filas de presentaciones 139 self._scroll_area = QScrollArea() 140 self._scroll_area.setWidgetResizable(True) 141 self._scroll_area.setHorizontalScrollBarPolicy( 142 Qt.ScrollBarPolicy.ScrollBarAlwaysOff 143 ) 144 self._scroll_area.setMaximumHeight(160) 145 self._scroll_area.setFrameShape(QScrollArea.Shape.NoFrame) 146 self._scroll_area.setObjectName('results_scroll') 147 148 self._rows_widget = QWidget() 149 self._rows_widget.setObjectName('results_bar') 150 self._rows_layout = QVBoxLayout(self._rows_widget) 151 self._rows_layout.setContentsMargins(0, 2, 0, 4) 152 self._rows_layout.setSpacing(1) 153 self._rows_layout.addStretch() 154 155 self._scroll_area.setWidget(self._rows_widget) 156 vlay.addWidget(self._scroll_area) 157 158 return panel 159 160 def _clear_result_rows(self) -> None: 161 while self._rows_layout.count() > 1: # deja el stretch final 162 item = self._rows_layout.takeAt(0) 163 if item.widget(): 164 item.widget().deleteLater() 165 166 def _add_result_row(self, odp: Path, pdf: Path | None) -> None: 167 row = QWidget() 168 row.setObjectName('results_bar') 169 row.setFixedHeight(30) 170 hlay = QHBoxLayout(row) 171 hlay.setContentsMargins(12, 2, 8, 2) 172 hlay.setSpacing(6) 173 174 # Nombre del fichero (sin versión para legibilidad) 175 name_lbl = QLabel(odp.name) 176 name_lbl.setObjectName('results_label') 177 name_lbl.setToolTip(str(odp)) 178 hlay.addWidget(name_lbl, 1) 179 180 btn_odp = QPushButton(_('ODP')) 181 btn_odp.setObjectName('open_result_btn') 182 btn_odp.setFixedHeight(22) 183 btn_odp.clicked.connect(lambda _, p=odp: subprocess.Popen(['xdg-open', str(p)])) 184 hlay.addWidget(btn_odp) 185 186 if pdf and pdf.exists(): 187 btn_pdf = QPushButton(_('PDF')) 188 btn_pdf.setObjectName('open_result_btn') 189 btn_pdf.setFixedHeight(22) 190 btn_pdf.clicked.connect(lambda _, p=pdf: subprocess.Popen(['xdg-open', str(p)])) 191 hlay.addWidget(btn_pdf) 192 193 # Insertar antes del stretch 194 self._rows_layout.insertWidget(self._rows_layout.count() - 1, row) 195 196 # ── Colapsar/expandir ───────────────────────────────────────────────────── 197 198 def _toggle_collapse(self) -> None: 199 self._collapsed = not self._collapsed 200 if self._collapsed: 201 self._collapse_btn.setText('▲') 202 self._collapse_btn.setToolTip(_('Expandir consola')) 203 self.text_area.hide() 204 self._results_panel.hide() 205 self.setMaximumHeight(36) 206 else: 207 self._collapse_btn.setText('▼') 208 self._collapse_btn.setToolTip(_('Colapsar consola')) 209 self.text_area.show() 210 self.setMaximumHeight(16777215) 211 self.collapse_toggled.emit(self._collapsed) 212 213 # ── API pública ─────────────────────────────────────────────────────────── 214 215 def set_progress(self, current: int, total: int) -> None: 216 if total <= 0: 217 self._progress_bar.setVisible(False) 218 return 219 pct = int(current * 100 / total) 220 self._progress_bar.setValue(pct) 221 self._progress_bar.setVisible(pct < 100) 222 223 def show_build_results(self, files: list) -> None: 224 """Muestra los ficheros generados. files: list[tuple[Path, Path | None]].""" 225 if not files: 226 self._results_panel.setVisible(False) 227 return 228 229 self._odp_dir = files[0][0].parent 230 self._clear_result_rows() 231 for odp, pdf in files: 232 self._add_result_row(odp, pdf) 233 234 row_h = 30 235 content_h = len(files) * (row_h + 1) + 8 236 self._scroll_area.setMaximumHeight(min(content_h, 160)) 237 self._results_panel.setVisible(True) 238 239 def hide_open_buttons(self) -> None: 240 self._results_panel.setVisible(False) 241 self._progress_bar.setValue(0) 242 self._progress_bar.setVisible(False) 243 244 def append_line(self, text: str, level: str = 'info') -> None: 245 color = self._colors.get(level, self._colors['info']) 246 self.text_area.appendHtml( 247 f'<span style="color:{color};white-space:pre;">{_escape(text)}</span>' 248 ) 249 self.text_area.moveCursor(QTextCursor.MoveOperation.End) 250 251 def clear_log(self) -> None: 252 self.text_area.clear() 253 254 def toPlainText(self) -> str: 255 return self.text_area.toPlainText() 256 257 # ── Internos ────────────────────────────────────────────────────────────── 258 259 def _open_folder(self) -> None: 260 if self._odp_dir and self._odp_dir.exists(): 261 subprocess.Popen(['xdg-open', str(self._odp_dir)]) 262 263 def _copy(self) -> None: 264 from PySide6.QtWidgets import QApplication 265 QApplication.clipboard().setText(self.text_area.toPlainText()) 266 267 def _refresh_log_colors(self) -> None: 268 self._colors = { 269 'ok': c('log_ok'), 270 'error': c('log_error'), 271 'info': c('log_info'), 272 } 273 274 def _on_theme_changed(self) -> None: 275 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
223 def show_build_results(self, files: list) -> None: 224 """Muestra los ficheros generados. files: list[tuple[Path, Path | None]].""" 225 if not files: 226 self._results_panel.setVisible(False) 227 return 228 229 self._odp_dir = files[0][0].parent 230 self._clear_result_rows() 231 for odp, pdf in files: 232 self._add_result_row(odp, pdf) 233 234 row_h = 30 235 content_h = len(files) * (row_h + 1) + 8 236 self._scroll_area.setMaximumHeight(min(content_h, 160)) 237 self._results_panel.setVisible(True)
Muestra los ficheros generados. files: list[tuple[Path, Path | None]].