debugVarTool/Src/setupVars_GUI.py

604 lines
24 KiB
Python
Raw 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.

# build command
# pyinstaller --onefile --name DebugVarEdit --add-binary "build/libclang.dll;build" --distpath ./ --workpath ./build_temp --specpath ./build_temp setupVars_GUI.py
import sys
import os
import subprocess
import xml.etree.ElementTree as ET
from generateVars import type_map
from enum import IntEnum
import threading
from scanVars import run_scan
from generateVars import run_generate
from setupVars import *
from VariableSelector import *
from VariableTable import VariableTableWidget, rows
from PySide6.QtWidgets import (
QApplication, QWidget, QTableWidget, QTableWidgetItem,
QCheckBox, QComboBox, QLineEdit, QVBoxLayout, QHBoxLayout, QPushButton,
QCompleter, QAbstractItemView, QLabel, QMessageBox, QFileDialog, QTextEdit,
QDialog, QTreeWidget, QTreeWidgetItem, QSizePolicy
)
from PySide6.QtGui import QTextCursor, QKeyEvent
from PySide6.QtCore import Qt, QProcess, QObject, Signal, QSettings
class EmittingStream(QObject):
text_written = Signal(str)
def __init__(self):
super().__init__()
self._buffer = ""
def write(self, text):
self._buffer += text
while '\n' in self._buffer:
line, self._buffer = self._buffer.split('\n', 1)
# Отправляем строку без '\n'
self.text_written.emit(line)
def flush(self):
if self._buffer:
self.text_written.emit(self._buffer)
self._buffer = ""
class ProcessOutputWindowDummy(QDialog):
def __init__(self, on_done_callback):
super().__init__()
self.setWindowTitle("Поиск переменных...")
self.resize(600, 400)
self.setModal(True) # сделаем окно модальным
self.layout = QVBoxLayout(self)
self.output_edit = QTextEdit()
self.output_edit.setReadOnly(True)
self.layout.addWidget(self.output_edit)
self.btn_close = QPushButton("Закрыть")
self.btn_close.setEnabled(False)
self.layout.addWidget(self.btn_close)
self.btn_close.clicked.connect(self.__handle_done)
self._on_done_callback = on_done_callback
def __handle_done(self):
if self._on_done_callback:
self._on_done_callback()
self.accept() # закрыть диалог
def append_text(self, text):
cursor = self.output_edit.textCursor()
cursor.movePosition(QTextCursor.End)
for line in text.splitlines():
self.output_edit.append(line)
self.output_edit.setTextCursor(cursor)
self.output_edit.ensureCursorVisible()
# 3. UI: таблица с переменными
class VarEditor(QWidget):
def __init__(self):
super().__init__()
self.vars_list = []
self.structs = {}
self.typedef_map = {}
self.proj_path = None
self.xml_path = None
self.makefile_path = None
self.structs_path = None
self.output_path = None
self._updating = False # Флаг блокировки рекурсии
self._resizing = False # флаг блокировки повторного вызова
self.initUI()
def initUI(self):
self.setWindowTitle("Variable Editor")
# --- Поля ввода пути проекта и XML ---
# XML Output
xml_layout = QHBoxLayout()
xml_layout.addWidget(QLabel("XML Output:"))
self.xml_output_edit = QLineEdit()
self.xml_output_edit.returnPressed.connect(self.update)
self.xml_output_edit.textChanged.connect(self.__on_xml_path_changed)
xml_layout.addWidget(self.xml_output_edit)
btn_xml_browse = QPushButton("...")
btn_xml_browse.setFixedWidth(30)
xml_layout.addWidget(btn_xml_browse)
btn_xml_browse.clicked.connect(self.__browse_xml_output)
# Project Path
proj_layout = QHBoxLayout()
proj_layout.addWidget(QLabel("Project Path:"))
self.proj_path_edit = QLineEdit()
self.proj_path_edit.returnPressed.connect(self.update)
self.proj_path_edit.textChanged.connect(self.__on_proj_path_changed)
proj_layout.addWidget(self.proj_path_edit)
btn_proj_browse = QPushButton("...")
btn_proj_browse.setFixedWidth(30)
proj_layout.addWidget(btn_proj_browse)
btn_proj_browse.clicked.connect(self.__browse_proj_path)
# Makefile Path
makefile_layout = QHBoxLayout()
makefile_layout.addWidget(QLabel("Makefile Path (relative path):"))
self.makefile_edit = QLineEdit()
self.makefile_edit.returnPressed.connect(self.update)
self.makefile_edit.textChanged.connect(self.__on_makefile_path_changed)
makefile_layout.addWidget(self.makefile_edit)
btn_makefile_browse = QPushButton("...")
btn_makefile_browse.setFixedWidth(30)
makefile_layout.addWidget(btn_makefile_browse)
btn_makefile_browse.clicked.connect(self.__browse_makefile)
# Source Output File/Directory
source_output_layout = QHBoxLayout()
source_output_layout.addWidget(QLabel("Source Output File:"))
self.source_output_edit = QLineEdit()
source_output_layout.addWidget(self.source_output_edit)
btn_source_output_browse = QPushButton("...")
btn_source_output_browse.setFixedWidth(30)
source_output_layout.addWidget(btn_source_output_browse)
btn_source_output_browse.clicked.connect(self.__browse_source_output)
self.btn_update_vars = QPushButton("Обновить данные о переменных")
self.btn_update_vars.clicked.connect(self.update_vars_data)
# Кнопка сохранения
btn_save = QPushButton("Build")
btn_save.clicked.connect(self.save_build)
# Кнопка добавления переменных
self.btn_add_vars = QPushButton("Add Variables")
self.btn_add_vars.clicked.connect(self.__open_variable_selector)
# Кнопка открыть output-файл с выбором программы
btn_open_output = QPushButton("Открыть Output File...")
btn_open_output.clicked.connect(self.__open_output_file_with_program)
# Таблица
self.table = VariableTableWidget()
# Основной layout
layout = QVBoxLayout()
layout.addLayout(xml_layout)
layout.addLayout(proj_layout)
layout.addLayout(makefile_layout)
layout.addWidget(self.btn_update_vars)
layout.addWidget(self.table)
layout.addWidget(self.btn_add_vars)
layout.addLayout(source_output_layout)
layout.addWidget(btn_save)
layout.addWidget(btn_open_output)
self.setLayout(layout)
def get_xml_path(self):
xml_path = self.xml_output_edit.text().strip()
return xml_path
def get_proj_path(self):
proj_path = self.proj_path_edit.text().strip()
return proj_path
def get_makefile_path(self):
proj_path = self.get_proj_path()
makefile_path = make_absolute_path(self.makefile_edit.text().strip(), proj_path)
return makefile_path
def get_struct_path(self):
proj_path = self.get_proj_path()
xml_path = self.get_xml_path()
root, tree = safe_parse_xml(xml_path)
if root is None:
return
# --- structs_path из атрибута ---
structs_path = root.attrib.get('structs_path', '').strip()
structs_path_full = make_absolute_path(structs_path, proj_path)
if structs_path_full and os.path.isfile(structs_path_full):
structs_path = structs_path_full
else:
structs_path = None
return structs_path
def get_output_path(self):
output_path = os.path.abspath(self.source_output_edit.text().strip())
return output_path
def update_all_paths(self):
self.proj_path = self.get_proj_path()
self.xml_path = self.get_xml_path()
self.makefile_path = self.get_makefile_path()
self.structs_path = self.get_struct_path()
self.output_path = self.get_output_path()
def update_vars_data(self):
self.update_all_paths()
if not self.proj_path or not self.xml_path:
QMessageBox.warning(self, "Ошибка", "Укажите пути проекта и XML.")
return
if not os.path.isfile(self.makefile_path):
QMessageBox.warning(self, "Ошибка", f"Makefile не найден:\n{self.makefile_path}")
return
# Создаём окно с кнопкой "Готово"
self.proc_win = ProcessOutputWindowDummy(self.__after_scanvars_finished)
self.proc_win.show()
self.emitting_stream = EmittingStream()
self.emitting_stream.text_written.connect(self.proc_win.append_text)
def run_scan_wrapper():
try:
old_stdout = sys.stdout
sys.stdout = self.emitting_stream
run_scan(self.proj_path, self.makefile_path, self.xml_path)
except Exception as e:
self.emitting_stream.text_written.emit(f"\n[ОШИБКА] {e}")
finally:
sys.stdout = old_stdout
self.emitting_stream.text_written.emit("\n--- Анализ завершён ---")
self.proc_win.btn_close.setEnabled(True)
threading.Thread(target=run_scan_wrapper, daemon=True).start()
def save_build(self):
vars_out = []
for row in range(self.table.rowCount()):
include_cb = self.table.cellWidget(row, rows.include)
if not include_cb.isChecked():
continue
name_edit = self.table.cellWidget(row, rows.name)
pt_type_combo = self.table.cellWidget(row, rows.pt_type)
iq_combo = self.table.cellWidget(row, rows.iq_type)
ret_combo = self.table.cellWidget(row, rows.ret_type)
short_name_edit = self.table.cellWidget(row, rows.short_name)
var_data = {
'name': name_edit.text(),
'type': 'pt_' + pt_type_combo.currentText(),
'iq_type': iq_combo.currentText(),
'return_type': ret_combo.currentText() if ret_combo.currentText() else 'int',
'short_name': short_name_edit.text(),
}
vars_out.append(var_data)
self.update_all_paths()
if not self.proj_path or not self.xml_path or not self.output_path:
QMessageBox.warning(self, "Ошибка", "Заполните все пути: проект, XML и output.")
return
try:
run_generate(self.proj_path, self.xml_path, self.output_path)
QMessageBox.information(self, "Готово", "Файл debug_vars.c успешно сгенерирован.")
self.update()
except Exception as e:
QMessageBox.critical(self, "Ошибка при генерации", str(e))
def update(self):
if self._updating:
return # Уже в процессе обновления — выходим, чтобы избежать рекурсии
self._updating = True
self.update_all_paths()
try:
if self.xml_path and not os.path.isfile(self.xml_path):
return
try:
root, tree = safe_parse_xml(self.xml_path)
if root is None:
return
if not self.proj_path:
# Если в поле ничего нет, пробуем взять из XML
proj_path_from_xml = root.attrib.get('proj_path', '').strip()
if proj_path_from_xml and os.path.isdir(proj_path_from_xml):
self.proj_path = proj_path_from_xml
self.proj_path_edit.setText(proj_path_from_xml)
else:
QMessageBox.warning(
self,
"Внимание",
"Путь к проекту (proj_path) не найден или не существует.\n"
"Пожалуйста, укажите его вручную в поле 'Project Path'."
)
else:
if not os.path.isdir(self.proj_path):
QMessageBox.warning(
self,
"Внимание",
f"Указанный путь к проекту не существует:\n{self.proj_path}\n"
"Пожалуйста, исправьте путь в поле 'Project Path'."
)
if not self.makefile_path:
# --- makefile_path из атрибута ---
makefile_path = root.attrib.get('makefile_path', '').strip()
makefile_path_full = make_absolute_path(makefile_path, self.proj_path)
if makefile_path_full and os.path.isfile(makefile_path_full):
self.makefile_edit.setText(makefile_path)
else:
self.makefile_path = None
# --- structs_path из атрибута ---
structs_path = root.attrib.get('structs_path', '').strip()
structs_path_full = make_absolute_path(structs_path, self.proj_path)
if structs_path_full and os.path.isfile(structs_path_full):
self.structs_path = structs_path_full
self.structs, self.typedef_map = parse_structs(structs_path_full)
else:
self.structs_path = None
self.vars_list = parse_vars(self.xml_path, self.typedef_map)
self.table.populate(self.vars_list, self.structs, self.write_to_xml)
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Ошибка при чтении XML:\n{e}")
finally:
self._updating = False # Снимаем блокировку при выходе из функции
def __browse_proj_path(self):
dir_path = QFileDialog.getExistingDirectory(self, "Выберите папку проекта")
if dir_path:
self.proj_path_edit.setText(dir_path)
self.proj_path = dir_path
if self.makefile_path and self.proj_path:
path = make_relative_path(self.makefile_path, self.proj_path)
self.makefile_edit.setText(path)
self.makefile_path = path
def __browse_xml_output(self):
file_path, _ = QFileDialog.getSaveFileName(
self,
"Выберите XML файл",
filter="XML files (*.xml);;All Files (*)"
)
self.xml_output_edit.setText(file_path)
self.xml_path = file_path
def keyPressEvent(self, event: QKeyEvent):
if event.key() == Qt.Key_Delete:
self.delete_selected_rows()
else:
super().keyPressEvent(event)
def __browse_makefile(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Выберите Makefile", filter="Makefile (makefile);;All Files (*)"
)
if file_path and self.proj_path:
path = make_relative_path(file_path, self.proj_path)
else:
path = file_path
self.makefile_edit.setText(path)
self.makefile_path = path
def __browse_source_output(self):
dir_path = QFileDialog.getExistingDirectory(self, "Выберите папку для debug_vars.c")
if dir_path:
self.source_output_edit.setText(dir_path)
self.output_path = dir_path
else:
self.output_path = ''
def __on_xml_path_changed(self):
self.xml_path = self.get_xml_path()
self.update()
def __on_proj_path_changed(self):
self.proj_path = self.get_proj_path()
self.update()
def __on_makefile_path_changed(self):
self.makefile_path = self.get_makefile_path()
if self.makefile_path and self.proj_path:
path = make_relative_path(self.makefile_path, self.proj_path)
self.makefile_edit.setText(path)
self.update()
def __after_scanvars_finished(self):
self.update_all_paths()
if not os.path.isfile(self.xml_path):
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {self.xml_path}")
return
try:
self.makefile_path = None
self.structs_path = None
self.proj_path = None
self.update()
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить переменные:\n{e}")
def delete_selected_rows(self):
selected_rows = sorted(set(index.row() for index in self.table.selectedIndexes()), reverse=True)
if not selected_rows:
return
# Удаляем из vars_list те, у кого show_var == true и имя совпадает
filtered_vars = [v for v in self.vars_list if v.get('show_var', 'false') == 'true']
for row in selected_rows:
if 0 <= row < len(filtered_vars):
var_to_remove = filtered_vars[row]
for v in self.vars_list:
if v['name'] == var_to_remove['name']:
v['show_var'] = 'false'
break
self.table.populate(self.vars_list, self.structs, self.write_to_xml)
def __open_variable_selector(self):
if not self.vars_list:
QMessageBox.warning(self, "Нет переменных", "Сначала загрузите или обновите переменные.")
return
dlg = VariableSelectorDialog(self.vars_list, self.structs, self.typedef_map, self.xml_path, self)
if dlg.exec():
self.write_to_xml()
self.update()
def write_to_xml(self):
self.update_all_paths()
if not self.xml_path or not os.path.isfile(self.xml_path):
print("XML файл не найден или путь пустой")
return
if not self.proj_path or not os.path.isdir(self.proj_path):
print("Project path не найден или путь пустой")
return
if not self.makefile_path or not os.path.isfile(self.makefile_path):
print("makefile файл не найден или путь пустой")
return
try:
root, tree = safe_parse_xml(self.xml_path)
if root is None:
return
root.set("proj_path", self.proj_path.replace("\\", "/"))
if self.makefile_path and os.path.isfile(self.makefile_path):
rel_makefile = make_relative_path(self.makefile_path, self.proj_path)
root.set("makefile_path", rel_makefile)
if self.structs_path and os.path.isfile(self.structs_path):
rel_struct = make_relative_path(self.structs_path, self.proj_path)
root.set("structs_path", rel_struct)
vars_elem = root.find('variables')
if vars_elem is None:
vars_elem = ET.SubElement(root, 'variables')
original_info = {}
for var_elem in vars_elem.findall('var'):
name = var_elem.attrib.get('name')
if name:
original_info[name] = {
'file': var_elem.findtext('file', ''),
'extern': var_elem.findtext('extern', ''),
'static': var_elem.findtext('static', '')
}
# Читаем переменные из таблицы (активные/изменённые)
table_vars = {v['name']: v for v in self.table.read_data()}
# Все переменные (в том числе новые, которых нет в таблице)
all_vars_by_name = {v['name']: v for v in self.vars_list}
# Объединённый список переменных для записи
all_names = list(all_vars_by_name.keys())
for name in all_names:
v = all_vars_by_name[name]
v_table = table_vars.get(name)
var_elem = None
# Ищем уже существующий <var> в XML
for ve in vars_elem.findall('var'):
if ve.attrib.get('name') == name:
var_elem = ve
break
if var_elem is None:
var_elem = ET.SubElement(vars_elem, 'var', {'name': name})
def set_sub_elem_text(parent, tag, text):
el = parent.find(tag)
if el is None:
el = ET.SubElement(parent, tag)
el.text = str(text)
show_var_val = str(v.get('show_var', 'false')).lower()
enable_val = str(v_table['enable'] if v_table and 'enable' in v_table else v.get('enable', 'false')).lower()
set_sub_elem_text(var_elem, 'show_var', show_var_val)
set_sub_elem_text(var_elem, 'enable', enable_val)
# Тут подтягиваем из таблицы, если есть, иначе из v
shortname_val = v_table['shortname'] if v_table and 'shortname' in v_table else v.get('shortname', '')
pt_type_val = v_table['pt_type'] if v_table and 'pt_type' in v_table else v.get('pt_type', '')
iq_type_val = v_table['iq_type'] if v_table and 'iq_type' in v_table else v.get('iq_type', '')
ret_type_val = v_table['return_type'] if v_table and 'return_type' in v_table else v.get('return_type', '')
set_sub_elem_text(var_elem, 'shortname', shortname_val)
set_sub_elem_text(var_elem, 'pt_type', pt_type_val)
set_sub_elem_text(var_elem, 'iq_type', iq_type_val)
set_sub_elem_text(var_elem, 'return_type', ret_type_val)
set_sub_elem_text(var_elem, 'type', v.get('type', ''))
# file/extern/static: из original_info, либо из v
file_val = v.get('file') or original_info.get(name, {}).get('file', '')
extern_val = v.get('extern') or original_info.get(name, {}).get('extern', '')
static_val = v.get('static') or original_info.get(name, {}).get('static', '')
set_sub_elem_text(var_elem, 'file', file_val)
set_sub_elem_text(var_elem, 'extern', extern_val)
set_sub_elem_text(var_elem, 'static', static_val)
ET.indent(tree, space=" ", level=0)
tree.write(self.xml_path, encoding='utf-8', xml_declaration=True)
except Exception as e:
print(f"Ошибка при сохранении XML: {e}")
def __open_output_file_with_program(self):
output_path = self.get_output_path()
if not output_path:
QMessageBox.warning(self, "Ошибка", "Путь к output-файлу не задан.")
return
output_file = os.path.join(output_path, "debug_vars.c")
if not os.path.isfile(output_file):
QMessageBox.warning(self, "Ошибка", f"Файл не найден:\n{output_file}")
return
try:
# Открыть стандартное окно Windows "Открыть с помощью..."
subprocess.run([
"rundll32.exe",
"shell32.dll,OpenAs_RunDLL",
output_file
], shell=False)
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось открыть диалог 'Открыть с помощью':\n{e}")
if __name__ == "__main__":
app = QApplication(sys.argv)
editor = VarEditor()
editor.resize(900, 600)
editor.show()
sys.exit(app.exec())