from __future__ import annotations import re from PySide6.QtGui import QTextCursor from PySide6.QtWidgets import QMessageBox, QPlainTextEdit from main_window.constants import _NEXT_CHECK_RE class UIHelpersMixin: def _safe(self, fn, *, title: str = "Error"): try: return fn() except Exception as e: # pragma: no cover - GUI try: self.ctrl.log_gui(f"[ui-error] {title}: {e}") except Exception: pass QMessageBox.critical(self, title, str(e)) return None def _set_text(self, widget: QPlainTextEdit, text: str, *, preserve_scroll: bool = False) -> None: """Set text, optionally сохраняя положение скролла (для trace).""" if not preserve_scroll: widget.setPlainText(text) return sb = widget.verticalScrollBar() old_max = sb.maximum() old_val = sb.value() at_end = old_val >= old_max - 2 widget.setPlainText(text) new_max = sb.maximum() if at_end: sb.setValue(new_max) else: # подвинем на ту же относительную позицию, учитывая прирост размера sb.setValue(max(0, min(new_max, old_val+(new_max-old_max)))) def _append_text(self, widget: QPlainTextEdit, text: str) -> None: cursor = widget.textCursor() cursor.movePosition(QTextCursor.End) cursor.insertText(text) widget.setTextCursor(cursor) widget.ensureCursorVisible() def _clean_ui_lines(self, lines) -> str: buf = "\n".join([str(x) for x in (lines or [])]).replace("\r", "\n") out_lines = [] for ln in buf.splitlines(): t = ln.strip() if not t: continue t2 = _NEXT_CHECK_RE.sub("", t).strip() if not t2: continue out_lines.append(t2) return "\n".join(out_lines).rstrip()