zapret-kvn/xray_fluent/ui/logs_page.py
loop-uh 774fa0bd21
Some checks failed
Windows project source guards / test (push) Failing after 45s
Кнопки: подпись уходит в подсказку, когда ряду не хватает места
- AdaptiveButtonRow: следит за шириной ряда (с учётом переполнения
  прокручиваемой страницы) и сворачивает кнопки со значком с конца ряда в
  квадратные значки, текст — в подсказку; появится место — возвращает.
- fluent_fixes: у PushButton в режиме «только значок» значок рисуется по
  центру (оригинал сдвигал его под подпись — значок съезжал и обрезался).
- Применено: строка сервера на панели (вместо кнопок-близнецов), панель
  инструментов подписок, логов и конфигов; их кнопкам добавлены значки.
Проверено рендером на win10: на 860 px «Сбросить к шаблону» сворачивается
в значок, остальные ряды помещаются с подписями.
2026-09-26 18:25:16 +03:00

131 lines
5.3 KiB
Python
Raw Permalink 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.

from __future__ import annotations
from PyQt6.QtCore import QTimer, pyqtSignal
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import BodyLabel, PlainTextEdit, PrimaryPushButton, PushButton, SearchLineEdit, SubtitleLabel
from qfluentwidgets import FluentIcon as FIF
from .adaptive_buttons import AdaptiveButtonRow
class LogsPage(QWidget):
clear_requested = pyqtSignal()
export_diag_requested = pyqtSignal()
def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self.setObjectName("logs")
self._lines: list[str] = []
self._pending_lines: list[str] = []
self._error_lines: list[str] = []
self._error_records: tuple = ()
self._error_lines_stale = False
self._full_refresh_needed = False
root = QVBoxLayout(self)
root.setContentsMargins(24, 20, 24, 20)
root.setSpacing(12)
root.addWidget(SubtitleLabel("Логи и диагностика", self))
toolbar = QHBoxLayout()
self.search = SearchLineEdit(self)
self.search.setPlaceholderText("Фильтр логов")
self.clear_btn = PushButton(FIF.BROOM, "Очистить", self)
self.export_btn = PrimaryPushButton(FIF.SHARE, "Экспорт диагностики", self)
toolbar.addWidget(self.search, 1)
self.errors_btn = PushButton(FIF.INFO, "Ошибки ядер", self)
self.errors_btn.setCheckable(True)
toolbar.addWidget(self.errors_btn)
toolbar.addWidget(self.clear_btn)
toolbar.addWidget(self.export_btn)
root.addLayout(toolbar)
# Не хватает места — подписи уходят в подсказки, остаются значки.
self._toolbar_fit = AdaptiveButtonRow(toolbar, [self.export_btn, self.errors_btn, self.clear_btn])
root.addWidget(BodyLabel("Логи работы", self))
self.log_edit = PlainTextEdit(self)
self.log_edit.setReadOnly(True)
self.log_edit.document().setMaximumBlockCount(2000)
root.addWidget(self.log_edit, 1)
self._refresh_timer = QTimer(self)
self._refresh_timer.setSingleShot(True)
self._refresh_timer.setInterval(200)
self._refresh_timer.timeout.connect(self._flush_updates)
self.search.textChanged.connect(self._schedule_full_refresh)
self.errors_btn.toggled.connect(self._schedule_full_refresh)
self.clear_btn.clicked.connect(self.clear_requested)
self.export_btn.clicked.connect(self.export_diag_requested)
def append_line(self, line: str) -> None:
self._lines.append(line)
if len(self._lines) > 5000:
self._lines = self._lines[-5000:]
self._pending_lines.append(line)
if self.search.text().strip():
self._full_refresh_needed = True
self._schedule_flush()
def set_lines(self, lines: list[str]) -> None:
self._lines = list(lines)
self._pending_lines.clear()
self._schedule_full_refresh()
def set_error_records(self, records) -> None:
# Форматируем лениво — только когда открыта вкладка «Ошибки ядер».
self._error_records = tuple(records)
self._error_lines_stale = True
if self.errors_btn.isChecked():
self._schedule_full_refresh()
def _formatted_error_lines(self) -> list[str]:
if getattr(self, "_error_lines_stale", False):
self._error_lines = [
f"[{r.failure.component}][{r.failure.stage}] {r.failure.message}\n"
f"{r.failure.code}; событий: {r.occurrences}"
for r in self._error_records
]
self._error_lines_stale = False
return self._error_lines
def clear_view(self) -> None:
self._lines = []
self._pending_lines.clear()
self._full_refresh_needed = False
self._refresh_timer.stop()
self.log_edit.clear()
def _schedule_flush(self) -> None:
if not self._refresh_timer.isActive():
self._refresh_timer.start()
def _schedule_full_refresh(self) -> None:
self._full_refresh_needed = True
self._schedule_flush()
def _flush_updates(self) -> None:
query = self.search.text().strip().lower()
if self.errors_btn.isChecked():
self.log_edit.document().setMaximumBlockCount(0)
self.log_edit.setPlainText("\n\n".join(
line for line in self._formatted_error_lines() if not query or query in line.lower()
))
self._pending_lines.clear()
self._full_refresh_needed = False
return
self.log_edit.document().setMaximumBlockCount(2000)
if self._full_refresh_needed or query:
if not query:
data = self._lines
else:
data = [line for line in self._lines if query in line.lower()]
self.log_edit.setPlainText("\n".join(data[-2000:]))
else:
if self._pending_lines:
self.log_edit.appendPlainText("\n".join(self._pending_lines))
self._pending_lines.clear()
self._full_refresh_needed = False
vbar = self.log_edit.verticalScrollBar()
vbar.setValue(vbar.maximum())