#!/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()