debugVarTool/Src/DebugVarEdit_GUI.py
Razvalyaev 369cfa808c чистка для релиза
переструктурировано чуть

и всякие мелкие фиксы
2025-07-15 15:37:29 +03:00

627 lines
26 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 var_setup_GUI.py
import sys
import os
import subprocess
import lxml.etree as ET
from generate_debug_vars import type_map
from enum import IntEnum
import threading
from generate_debug_vars import run_generate
import var_setup
from var_selector_window import VariableSelectorDialog
from var_table import VariableTableWidget, rows
from scan_progress_gui import ProcessOutputWindow
import scan_vars
import myXML
import time
from PySide2.QtWidgets import (
QApplication, QWidget, QTableWidget, QTableWidgetItem,
QCheckBox, QComboBox, QLineEdit, QVBoxLayout, QHBoxLayout, QPushButton,
QCompleter, QAbstractItemView, QLabel, QMessageBox, QFileDialog, QTextEdit,
QDialog, QTreeWidget, QTreeWidgetItem, QSizePolicy, QHeaderView
)
from PySide2.QtGui import QTextCursor, QKeyEvent, QIcon, QFont
from PySide2.QtCore import Qt, QProcess, QObject, Signal, QSettings
var_edit_title = "Редактор переменных для отладки"
xml_path_title = "Путь к XML:"
proj_path_title = "Путь к проекту:"
makefile_path_title = "Пусть к makefile (относительно проекта)"
output_path_title = "Путь для для debug_vars.c:"
scan_title = "Сканировать переменные"
build_title = "Сгенерировать файл"
add_vars_title = "Добавить переменные"
open_output_title = "Открыть файл"
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)
# 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(var_edit_title)
base_path = scan_vars.get_base_path()
icon_path = os.path.join(base_path, "icon.ico")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
# --- Поля ввода пути проекта и XML ---
# XML Output
xml_layout = QHBoxLayout()
xml_layout.addWidget(QLabel(xml_path_title))
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(proj_path_title))
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_title))
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(output_path_title))
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(scan_title)
self.btn_update_vars.clicked.connect(self.update_vars_data)
# Кнопка сохранения
btn_save = QPushButton(build_title)
btn_save.clicked.connect(self.save_build)
# Кнопка добавления переменных
self.btn_add_vars = QPushButton(add_vars_title)
self.btn_add_vars.clicked.connect(self.__open_variable_selector)
# Кнопка открыть output-файл с выбором программы
btn_open_output = QPushButton(open_output_title)
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()
rel_makefile_path = self.makefile_edit.text().strip()
if not rel_makefile_path:
return None
makefile_path = myXML.make_absolute_path(rel_makefile_path, proj_path)
return makefile_path
def get_struct_path(self):
proj_path = self.get_proj_path()
xml_path = self.get_xml_path()
root, tree = myXML.safe_parse_xml(xml_path)
if root is None:
return
# --- structs_path из атрибута ---
structs_path = root.attrib.get('structs_path', '').strip()
structs_path_full = myXML.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 = ProcessOutputWindow(self.proj_path, self.makefile_path, self.xml_path,
self.__after_scan_vars_finished, self)
self.proc_win.start_scan()
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': 't_' + iq_combo.currentText(),
'return_type': 't_' + ret_combo.currentText() if ret_combo.currentText() else 't_iq_none',
'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, "Ошибка", f"Заполните: {xml_path_title[:-1]}, {proj_path_title[:-1]}, {output_path_title[:-1]}.")
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, force=0):
if self._updating and (force==0):
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 = myXML.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,
"Внимание",
"Путь к проекту не найден или не существует.\n"
f"Пожалуйста, укажите его вручную в поле '{proj_path_title[:-1]}'."
)
else:
if not os.path.isdir(self.proj_path):
QMessageBox.warning(
self,
"Внимание",
f"Указанный путь к проекту не существует:\n{self.proj_path}\n"
"Пожалуйста, исправьте путь в поле '{proj_path_title[:-1]}'."
)
if not self.makefile_path and self.proj_path and os.path.isdir(self.proj_path):
makefile_path = root.attrib.get('makefile_path', '').strip()
makefile_path_full = myXML.make_absolute_path(makefile_path, self.proj_path)
if os.path.isfile(makefile_path_full):
# Обновляем edit-поле на относительный путь, абсолют сохраняем
self.makefile_path = makefile_path_full
self.makefile_edit.setText(makefile_path)
else:
self.makefile_path = None
self.makefile_edit.setText("")
# --- structs_path из атрибута ---
structs_path = root.attrib.get('structs_path', '').strip()
structs_path_full = myXML.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 = var_setup.parse_structs(structs_path_full)
else:
self.structs_path = None
self.vars_list = var_setup.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
# Сброс makefile, если proj_path изменился
if not os.path.isdir(dir_path):
self.makefile_path = None
self.makefile_edit.setText("")
else:
if self.makefile_path and os.path.isfile(self.makefile_path):
rel_path = myXML.make_relative_path(self.makefile_path, dir_path)
self.makefile_edit.setText(rel_path)
self.update()
self.update()
if self.makefile_path and self.proj_path:
path = myXML.make_relative_path(self.makefile_path, self.proj_path)
self.makefile_edit.setText(path)
self.makefile_path = path
def __browse_xml_output(self):
dialog = QFileDialog(self, "Выберите или создайте XML-файл")
dialog.setAcceptMode(QFileDialog.AcceptSave)
dialog.setNameFilter("XML files (*.xml);;All Files (*)")
dialog.setDefaultSuffix("xml")
dialog.setOption(QFileDialog.DontConfirmOverwrite, True) # ⚠️ Не спрашивать про перезапись
if dialog.exec_():
file_path = dialog.selectedFiles()[0]
if not file_path.endswith(".xml"):
file_path += ".xml"
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 = myXML.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()
if not os.path.isdir(self.proj_path):
self.makefile_path = None
self.makefile_edit.setText("")
return # Преждевременно выходим, если проект не существует
# Обновим путь к makefile, если он уже задан и абсолютен
if self.makefile_path and os.path.isfile(self.makefile_path):
rel_path = myXML.make_relative_path(self.makefile_path, self.proj_path)
self.makefile_edit.setText(rel_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 = myXML.make_relative_path(self.makefile_path, self.proj_path)
self.makefile_edit.setText(path)
self.update()
def __after_scan_vars_finished(self):
if not os.path.isfile(self.xml_path):
self.makefile_path = None
self.structs_path = None
self.proj_path = None
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {self.xml_path}")
return
try:
self.update(1)
except Exception as e:
self.makefile_path = None
self.structs_path = None
self.proj_path = None
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить переменные:\n{e}")
def delete_selected_rows(self):
# Получаем имена всех выбранных переменных из первого столбца
selected_names = self.table.get_selected_var_names()
if not selected_names:
return
# Меняем флаг show_var по имени
for var in self.vars_list:
if var.get('name') in selected_names:
var['show_var'] = 'false'
self.table.populate(self.vars_list, self.structs, self.write_to_xml)
self.write_to_xml()
def __open_variable_selector(self):
if not self.vars_list:
QMessageBox.warning(self, "Нет переменных", f"Сначала загрузите переменные ({scan_title}).")
return
self.update()
dlg = VariableSelectorDialog(self.table, 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, dummy=None):
t0 = time.time()
self.update_all_paths()
def get_val(name, default=''):
return str(v_table[name] if v_table and name in v_table else v.get(name, default))
def element_differs(elem, values: dict):
for tag, new_val in values.items():
current_elem = elem.find(tag)
current_val = (current_elem.text or '').strip()
new_val_str = str(new_val or '').strip()
if current_val != new_val_str:
return True
return False
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
if os.path.abspath(self.makefile_path) == os.path.abspath(self.proj_path):
print("makefile_path совпадает с proj_path — игнор")
return
try:
root, tree = myXML.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 = myXML.make_relative_path(self.makefile_path, self.proj_path)
# Если результат — абсолютный путь, не записываем
if not os.path.isabs(rel_makefile):
root.set("makefile_path", rel_makefile)
if self.structs_path and os.path.isfile(self.structs_path):
rel_struct = myXML.make_relative_path(self.structs_path, self.proj_path)
root.set("structs_path", rel_struct)
if not os.path.isabs(rel_struct):
root.set("structs_path", rel_struct)
t1 = time.time()
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', '')
}
var_elements_by_name = {ve.attrib.get('name'): ve for ve in vars_elem.findall('var')}
# Читаем переменные из таблицы (активные/изменённые)
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())
t2 = time.time()
for name in all_names:
v = all_vars_by_name[name]
v_table = table_vars.get(name)
var_elem = None
pt_type_val = get_val('pt_type').lower()
if 'arr' in pt_type_val or 'struct' in pt_type_val or 'union' in pt_type_val:
continue
show_var_val = str(v.get('show_var', 'false')).lower()
enable_val = get_val('enable').lower()
# Тут подтягиваем из таблицы, если есть, иначе из v
shortname_val = get_val('shortname')
iq_type_val = get_val('iq_type')
ret_type_val = get_val('return_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', '')
values_to_write = {
'show_var': show_var_val,
'enable': enable_val,
'shortname': shortname_val,
'pt_type': pt_type_val,
'iq_type': iq_type_val,
'return_type': ret_type_val,
'type': v.get('type', ''),
'file': file_val,
'extern': extern_val,
'static': static_val
}
# Ищем уже существующий <var> в XML
var_elem = var_elements_by_name.get(name)
# Если элемента нет, это новая переменная — сразу пишем
if var_elem is None:
var_elem = ET.SubElement(vars_elem, 'var', {'name': name})
var_elements_by_name[name] = var_elem
write_all = True # обязательно записать все поля
else:
write_all = element_differs(var_elem, values_to_write)
if not write_all:
continue # Пропускаем, если нет изменений
for tag, text in values_to_write.items():
set_sub_elem_text(var_elem, tag, text)
t3 = time.time()
# Преобразуем дерево в строку
myXML.fwrite(root, self.xml_path)
self.table.check()
t4 = time.time()
'''print(f"[T1] parse + set paths: {t1 - t0:.3f} сек")
print(f"[T2] prepare variables: {t2 - t1:.3f} сек")
print(f"[T3] loop + updates: {t3 - t2:.3f} сек")
print(f"[T4] write to file: {t4 - t3:.3f} сек")
print(f"[TOTAL] write_to_xml total: {t4 - t0:.3f} сек")'''
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,
"Ошибка",
"Путь к debug_var.c не задан."
f"Пожалуйста, укажите его в поле '{output_path_title[:-1]}'.")
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_())