#!/usr/bin/env python3
"""
PyQt5 GUI для Legacy Analyzer.
Запуск:
python gui.py
pip install PyQt5 # если не установлен
"""
import logging
import os
import sys
from pathlib import Path
from PyQt5.QtCore import QThread, pyqtSignal, Qt
from PyQt5.QtGui import QColor, QFont, QTextCursor
from PyQt5.QtWidgets import (
QApplication, QCheckBox, QFileDialog, QFormLayout, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMainWindow,
QPushButton, QSizePolicy, QSpinBox, QSplitter, QTableWidget,
QTableWidgetItem, QTextEdit, QVBoxLayout, QWidget,
)
# Добавляем директорию скрипта в sys.path, чтобы импорты работали при запуске
# gui.py из любой рабочей директории
_HERE = Path(__file__).parent
if str(_HERE) not in sys.path:
sys.path.insert(0, str(_HERE))
from config import AnalyzerConfig, ThresholdConfig
from analyzer import Analyzer
from models import RiskLevel, ModernizationStrategy
# ---------------------------------------------------------------------------
# Цвета уровней риска
# ---------------------------------------------------------------------------
RISK_COLORS = {
RiskLevel.LOW: "#27ae60",
RiskLevel.MEDIUM: "#f39c12",
RiskLevel.HIGH: "#e67e22",
RiskLevel.CRITICAL: "#e74c3c",
}
RISK_BG = {
RiskLevel.LOW: "#eafaf1",
RiskLevel.MEDIUM: "#fef9e7",
RiskLevel.HIGH: "#fef0e6",
RiskLevel.CRITICAL: "#fdedec",
}
STRATEGY_RU = {
ModernizationStrategy.KEEP: "Оставить",
ModernizationStrategy.REFACTOR: "Рефакторинг",
ModernizationStrategy.REENGINEER: "Реинжиниринг",
ModernizationStrategy.REPLACE: "Замена",
}
RISK_RU = {
RiskLevel.LOW: "НИЗКИЙ",
RiskLevel.MEDIUM: "СРЕДНИЙ",
RiskLevel.HIGH: "ВЫСОКИЙ",
RiskLevel.CRITICAL: "КРИТИЧЕСКИЙ",
}
LOG_LEVEL_COLORS = {
"DEBUG": "#888888",
"INFO": "#ecf0f1",
"WARNING": "#f39c12",
"ERROR": "#e74c3c",
"CRITICAL":"#e74c3c",
}
# ---------------------------------------------------------------------------
# Фоновый поток анализа
# ---------------------------------------------------------------------------
class _QtLogHandler(logging.Handler):
"""Перехватывает logging и отправляет сообщения в Qt-сигнал."""
def __init__(self, signal):
super().__init__()
self._signal = signal
def emit(self, record):
try:
self._signal.emit(self.format(record), record.levelname)
except Exception:
pass
class AnalysisWorker(QThread):
log_signal = pyqtSignal(str, str) # (message, levelname)
finished_signal = pyqtSignal(object) # AnalysisReport
error_signal = pyqtSignal(str)
def __init__(self, project_root: str, config: AnalyzerConfig):
super().__init__()
self._project_root = project_root
self._config = config
def run(self):
handler = _QtLogHandler(self.log_signal)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S"))
root_logger = logging.getLogger()
root_logger.addHandler(handler)
root_logger.setLevel(logging.DEBUG)
try:
analyzer = Analyzer(self._config)
report = analyzer.run(self._project_root)
self.finished_signal.emit(report)
except Exception as exc:
self.error_signal.emit(str(exc))
finally:
root_logger.removeHandler(handler)
# ---------------------------------------------------------------------------
# Панель настроек (левая)
# ---------------------------------------------------------------------------
class _SettingsPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._build_ui()
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(10)
# --- Проект ---
proj_box = QGroupBox("Проект")
proj_form = QFormLayout(proj_box)
proj_form.setLabelAlignment(Qt.AlignRight)
self.project_edit = QLineEdit()
self.project_edit.setPlaceholderText("Путь к анализируемому проекту")
proj_btn = QPushButton("…")
proj_btn.setFixedWidth(30)
proj_btn.clicked.connect(self._browse_project)
proj_row = QHBoxLayout()
proj_row.addWidget(self.project_edit)
proj_row.addWidget(proj_btn)
self.output_edit = QLineEdit("legacy_report")
self.output_edit.setPlaceholderText("Директория для отчётов")
out_btn = QPushButton("…")
out_btn.setFixedWidth(30)
out_btn.clicked.connect(self._browse_output)
out_row = QHBoxLayout()
out_row.addWidget(self.output_edit)
out_row.addWidget(out_btn)
self.libclang_edit = QLineEdit()
self.libclang_edit.setPlaceholderText("Авто (опционально)")
lc_btn = QPushButton("…")
lc_btn.setFixedWidth(30)
lc_btn.clicked.connect(self._browse_libclang)
lc_row = QHBoxLayout()
lc_row.addWidget(self.libclang_edit)
lc_row.addWidget(lc_btn)
proj_form.addRow("Проект:", proj_row)
proj_form.addRow("Вывод:", out_row)
proj_form.addRow("libclang:", lc_row)
# --- Форматы ---
fmt_box = QGroupBox("Форматы отчётов")
fmt_layout = QHBoxLayout(fmt_box)
self.fmt_json = QCheckBox("JSON")
self.fmt_json.setChecked(True)
self.fmt_html = QCheckBox("HTML")
self.fmt_html.setChecked(True)
fmt_layout.addWidget(self.fmt_json)
fmt_layout.addWidget(self.fmt_html)
fmt_layout.addStretch()
# --- Пороги ---
thr_box = QGroupBox("Пороговые значения")
thr_form = QFormLayout(thr_box)
thr_form.setLabelAlignment(Qt.AlignRight)
self.ccn_warn = self._spinbox(1, 100, 10)
self.ccn_crit = self._spinbox(1, 100, 20)
self.coup_warn = self._spinbox(1, 100, 5)
self.coup_crit = self._spinbox(1, 100, 10)
thr_form.addRow("CCN предупрежд.:", self.ccn_warn)
thr_form.addRow("CCN критическое:", self.ccn_crit)
thr_form.addRow("Связность вых. (пред.):", self.coup_warn)
thr_form.addRow("Связность вых. (крит.):", self.coup_crit)
# --- Кнопка запуска ---
self.run_btn = QPushButton("▶ Запустить анализ")
self.run_btn.setMinimumHeight(40)
self.run_btn.setStyleSheet(
"QPushButton { background: #2980b9; color: white; font-weight: bold; "
"border-radius: 4px; font-size: 13px; }"
"QPushButton:hover { background: #3498db; }"
"QPushButton:disabled { background: #7f8c8d; }"
)
layout.addWidget(proj_box)
layout.addWidget(fmt_box)
layout.addWidget(thr_box)
layout.addStretch()
layout.addWidget(self.run_btn)
@staticmethod
def _spinbox(minimum, maximum, default) -> QSpinBox:
sb = QSpinBox()
sb.setRange(minimum, maximum)
sb.setValue(default)
return sb
def _browse_project(self):
d = QFileDialog.getExistingDirectory(self, "Выберите директорию проекта")
if d:
self.project_edit.setText(d)
def _browse_output(self):
d = QFileDialog.getExistingDirectory(self, "Директория для отчётов")
if d:
self.output_edit.setText(d)
def _browse_libclang(self):
f, _ = QFileDialog.getOpenFileName(self, "Путь к libclang", "",
"Библиотеки (*.so *.dylib *.dll);;Все файлы (*)")
if f:
self.libclang_edit.setText(f)
def build_config(self) -> AnalyzerConfig:
formats = []
if self.fmt_json.isChecked():
formats.append("json")
if self.fmt_html.isChecked():
formats.append("html")
if not formats:
formats = ["json"]
thresholds = ThresholdConfig(
ccn_warn=self.ccn_warn.value(),
ccn_critical=self.ccn_crit.value(),
coupling_out_warn=self.coup_warn.value(),
coupling_out_critical=self.coup_crit.value(),
)
libclang = self.libclang_edit.text().strip() or None
return AnalyzerConfig(
output_formats=formats,
output_dir=self.output_edit.text().strip() or "legacy_report",
libclang_path=libclang,
thresholds=thresholds,
)
# ---------------------------------------------------------------------------
# Панель лога (правая верхняя)
# ---------------------------------------------------------------------------
class _LogPanel(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
self.setFont(QFont("Monospace", 9))
self.setStyleSheet("background: #1e1e1e; color: #ecf0f1;")
def append_log(self, message: str, level: str):
color = LOG_LEVEL_COLORS.get(level, "#ecf0f1")
html = f'{message}'
self.append(html)
self.moveCursor(QTextCursor.End)
def clear_log(self):
self.clear()
# ---------------------------------------------------------------------------
# Панель результатов (правая нижняя)
# ---------------------------------------------------------------------------
class _ResultsPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._html_path: str = ""
self._build_ui()
self.setVisible(False)
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 4, 8, 8)
layout.setSpacing(6)
# Заголовок
title = QLabel("Результаты анализа")
title.setStyleSheet("font-weight: bold; font-size: 13px;")
layout.addWidget(title)
# Сводка
self._summary = QLabel()
self._summary.setWordWrap(True)
self._summary.setStyleSheet("font-size: 12px;")
layout.addWidget(self._summary)
# Таблица решений
self._table = QTableWidget()
self._table.setColumnCount(4)
self._table.setHorizontalHeaderLabels(["Модуль", "Риск", "Стратегия", "Причины"])
self._table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self._table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
self._table.setEditTriggers(QTableWidget.NoEditTriggers)
self._table.setSelectionBehavior(QTableWidget.SelectRows)
self._table.setAlternatingRowColors(True)
layout.addWidget(self._table)
# Кнопка открытия отчёта
self._open_btn = QPushButton("Открыть HTML отчёт")
self._open_btn.setStyleSheet(
"QPushButton { background: #27ae60; color: white; border-radius: 4px; padding: 6px; }"
"QPushButton:hover { background: #2ecc71; }"
"QPushButton:disabled { background: #7f8c8d; }"
)
self._open_btn.clicked.connect(self._open_html)
self._open_btn.setEnabled(False)
layout.addWidget(self._open_btn)
def show_report(self, report, output_dir: str):
from models import RiskLevel
risk = report.system_risk_level
strategy = report.recommended_system_strategy
color = RISK_COLORS.get(risk, "#ecf0f1")
risk_ru = RISK_RU.get(risk, risk.name)
strategy_ru = STRATEGY_RU.get(strategy, strategy.name)
summary_html = (
f"Системный риск: {risk_ru} | "
f"Стратегия: {strategy_ru}
"
f"Модулей: {len(report.modules)} | "
f"Функций: {report.total_functions} | "
f"Ср. CCN: {report.avg_system_ccn:.2f} | "
f"Циклов зависим.: {len(report.dependency_cycles)}"
)
self._summary.setText(summary_html)
# Заполняем таблицу
self._table.setRowCount(len(report.decisions))
for row, decision in enumerate(report.decisions):
r = decision.risk_level
bg = QColor(RISK_BG.get(r, "#ffffff"))
fg = QColor(RISK_COLORS.get(r, "#000000"))
items = [
decision.module_name,
RISK_RU.get(r, r.name),
STRATEGY_RU.get(decision.strategy, decision.strategy.name),
"; ".join(decision.reasons) if decision.reasons else "—",
]
for col, text in enumerate(items):
item = QTableWidgetItem(text)
item.setBackground(bg)
if col == 1:
item.setForeground(fg)
item.setFont(QFont("", -1, QFont.Bold))
self._table.setItem(row, col, item)
self._table.resizeRowsToContents()
# HTML отчёт
html_path = Path(output_dir) / "report.html"
if not html_path.is_absolute():
html_path = Path.cwd() / html_path
if html_path.exists():
self._html_path = str(html_path)
self._open_btn.setEnabled(True)
else:
self._html_path = ""
self._open_btn.setEnabled(False)
self.setVisible(True)
def _open_html(self):
if self._html_path:
import webbrowser
webbrowser.open(f"file://{self._html_path}")
# ---------------------------------------------------------------------------
# Главное окно
# ---------------------------------------------------------------------------
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Legacy Analyzer")
self.resize(1100, 700)
self._worker: AnalysisWorker | None = None
self._build_ui()
def _build_ui(self):
central = QWidget()
self.setCentralWidget(central)
root_layout = QHBoxLayout(central)
root_layout.setContentsMargins(6, 6, 6, 6)
root_layout.setSpacing(6)
# Левая панель — настройки
self._settings = _SettingsPanel()
self._settings.setFixedWidth(320)
self._settings.run_btn.clicked.connect(self._run_analysis)
# Правая панель — лог + результаты
right_splitter = QSplitter(Qt.Vertical)
self._log = _LogPanel()
self._log.setMinimumHeight(150)
self._results = _ResultsPanel()
self._results.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
right_splitter.addWidget(self._log)
right_splitter.addWidget(self._results)
right_splitter.setSizes([300, 300])
root_layout.addWidget(self._settings)
root_layout.addWidget(right_splitter, stretch=1)
def _run_analysis(self):
project_root = self._settings.project_edit.text().strip()
if not project_root:
self._log.append_log("Укажите путь к проекту.", "ERROR")
return
if not Path(project_root).exists():
self._log.append_log(f"Директория не найдена: {project_root}", "ERROR")
return
config = self._settings.build_config()
self._log.clear_log()
self._results.setVisible(False)
self._settings.run_btn.setEnabled(False)
self._settings.run_btn.setText("⏳ Анализ...")
self._worker = AnalysisWorker(project_root, config)
self._worker.log_signal.connect(self._log.append_log)
self._worker.finished_signal.connect(self._on_finished)
self._worker.error_signal.connect(self._on_error)
self._worker.start()
def _on_finished(self, report):
self._reset_run_btn()
output_dir = self._settings.output_edit.text().strip() or "legacy_report"
self._results.show_report(report, output_dir)
self._log.append_log("Анализ завершён.", "INFO")
def _on_error(self, message: str):
self._reset_run_btn()
self._log.append_log(f"Ошибка: {message}", "ERROR")
def _reset_run_btn(self):
self._settings.run_btn.setEnabled(True)
self._settings.run_btn.setText("▶ Запустить анализ")
# ---------------------------------------------------------------------------
# Точка входа
# ---------------------------------------------------------------------------
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()