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 PySide6.QtWidgets import ( QApplication, QWidget, QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QVBoxLayout, QHBoxLayout, QPushButton, QCompleter, QAbstractItemView, QLabel, QMessageBox, QFileDialog, QTextEdit, QDialog, QTreeWidget, QTreeWidgetItem ) from PySide6.QtGui import QTextCursor, QKeyEvent from PySide6.QtCore import Qt, QProcess, QObject, Signal, QTimer class rows(IntEnum): include = 0 name = 1 type = 2 pt_type = 3 iq_type = 4 ret_type = 5 short_name = 6 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(QWidget): def __init__(self, on_done_callback): super().__init__() self.setWindowTitle("Поиск переменных...") self.resize(600, 400) 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.close() 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.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.read_xml_file) 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.read_xml_file) 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.read_xml_file) 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) # Таблица переменных self.table = QTableWidget(len(self.vars_list), 7) self.table.setHorizontalHeaderLabels([ 'Include', 'Name', 'Origin Type', 'Pointer Type', 'IQ Type', 'Return Type', 'Short Name' ]) self.table.setEditTriggers(QAbstractItemView.AllEditTriggers) # Кнопка сохранения 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) # Основной 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) 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) # Здесь нужно указать абсолютные пути к проекту, xml и output (замени на свои) 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 успешно сгенерирован.") except Exception as e: QMessageBox.critical(self, "Ошибка при генерации", str(e)) def read_xml_file(self): self.update_all_paths() 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'." ) # --- 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) self.makefile_path = makefile_path_full # --- 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.update_table() except Exception as e: QMessageBox.warning(self, "Ошибка", f"Ошибка при чтении XML:\n{e}") 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.read_xml_file() def __on_proj_path_changed(self): self.proj_path = self.get_proj_path() self.read_xml_file() 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.makefile_path = path self.read_xml_file() def __after_scanvars_finished(self): xml_path = self.xml_output_edit.text().strip() if not os.path.isfile(xml_path): QMessageBox.critical(self, "Ошибка", f"Файл не найден: {xml_path}") return try: # Читаем структуры, если задан путь if self.structs_path and os.path.isfile(self.structs_path): try: self.structs, self.typedef_map = parse_structs(self.structs_path) # При необходимости обновите UI или сделайте что-то с self.structs except Exception as e: QMessageBox.warning(self, "Внимание", f"Не удалось загрузить структуры из {self.structs_path}:\n{e}") self.vars_list = parse_vars(xml_path) self.update_table() 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.update_table() 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) if dlg.exec(): self.write_to_xml() self.read_xml_file() self.update_table() def update_table(self): self.type_options = list(dict.fromkeys(type_map.values())) self.display_type_options = [t.replace('pt_', '') for t in self.type_options] iq_types = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)] filtered_vars = [v for v in self.vars_list if v.get('show_var', 'false') == 'true'] self.table.setRowCount(len(filtered_vars)) for row, var in enumerate(filtered_vars): cb = QCheckBox() enable_str = var.get('enable', 'false') cb.setChecked(enable_str.lower() == 'true') self.table.setCellWidget(row, rows.include, cb) name_edit = QLineEdit(var['name']) if var['type'] in self.structs: completer = QCompleter(self.structs[var['type']].keys()) completer.setCaseSensitivity(Qt.CaseInsensitive) name_edit.setCompleter(completer) self.table.setCellWidget(row, rows.name, name_edit) # Type (origin) origin_type = var.get('type', '').strip() origin_item = QTableWidgetItem(origin_type) origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) # read-only self.table.setItem(row, rows.type, origin_item) pt_type_combo = QComboBox() pt_type_combo.addItems(self.display_type_options) internal_type = var['pt_type'].replace('pt_', '') if internal_type in self.display_type_options: pt_type_combo.setCurrentText(internal_type) else: pt_type_combo.addItem(internal_type) pt_type_combo.setCurrentText(internal_type) self.table.setCellWidget(row, rows.pt_type, pt_type_combo) iq_combo = QComboBox() iq_combo.addItems(iq_types) iq_type = var['iq_type'].replace('t_', '') if iq_type in iq_types: iq_combo.setCurrentText(iq_type) else: iq_combo.addItem(iq_type) iq_combo.setCurrentText(iq_type) self.table.setCellWidget(row, rows.iq_type, iq_combo) ret_combo = QComboBox() ret_combo.addItems(iq_types) self.table.setCellWidget(row, rows.ret_type, ret_combo) short_name_edit = QLineEdit(var['name']) self.table.setCellWidget(row, rows.short_name, short_name_edit) cb.stateChanged.connect(self.write_to_xml) name_edit.textChanged.connect(self.write_to_xml) pt_type_combo.currentTextChanged.connect(self.write_to_xml) iq_combo.currentTextChanged.connect(self.write_to_xml) ret_combo.currentTextChanged.connect(self.write_to_xml) short_name_edit.textChanged.connect(self.write_to_xml) self.write_to_xml() def read_table(self): vars_data = [] for row in range(self.table.rowCount()): cb = self.table.cellWidget(row, rows.include) 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) origin_item = self.table.item(row, rows.type) vars_data.append({ 'show_var': True, 'enable': cb.isChecked() if cb else False, 'name': name_edit.text() if name_edit else '', 'pt_type': 'pt_' + pt_type_combo.currentText() if pt_type_combo else '', 'iq_type': iq_combo.currentText() if iq_combo else '', 'return_type': ret_combo.currentText() if ret_combo else '', 'shortname': short_name_edit.text() if short_name_edit else '', 'type': origin_item.text() if origin_item else '', }) return vars_data 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.read_table()} # Все переменные (в том числе новые, которых нет в XML) all_vars_by_name = {v['name']: v for v in self.vars_list} # Объединённый список переменных для записи all_names = set(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 # Ищем уже существующий в 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) set_sub_elem_text(var_elem, 'show_var', v.get('show_var', 'false')) set_sub_elem_text(var_elem, 'enable', v.get('enable', 'false')) set_sub_elem_text(var_elem, 'shortname', v['shortname']) set_sub_elem_text(var_elem, 'pt_type', v['pt_type']) set_sub_elem_text(var_elem, 'iq_type', v['iq_type']) set_sub_elem_text(var_elem, 'return_type', v['return_type']) set_sub_elem_text(var_elem, 'type', v['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}") if __name__ == "__main__": app = QApplication(sys.argv) editor = VarEditor() editor.resize(900, 600) editor.show() sys.exit(app.exec())