diff --git a/DebugVarEdit.exe b/DebugVarEdit.exe index d8da614..5c82030 100644 Binary files a/DebugVarEdit.exe and b/DebugVarEdit.exe differ diff --git a/Src/VariableSelector.py b/Src/VariableSelector.py index 406aab3..25cedd0 100644 --- a/Src/VariableSelector.py +++ b/Src/VariableSelector.py @@ -1,9 +1,10 @@ import re from PySide6.QtWidgets import ( QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton, - QLineEdit, QLabel, QHeaderView + QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout ) -from PySide6.QtCore import Qt +from PySide6.QtGui import QKeySequence, QKeyEvent +from PySide6.QtCore import Qt, QStringListModel, QSettings from setupVars import * from scanVars import * @@ -25,9 +26,21 @@ class VariableSelectorDialog(QDialog): self.xml_path = xml_path # сохраняем путь к xml + # --- Добавляем чекбокс для автодополнения --- + self.autocomplete_checkbox = QCheckBox("Включить автодополнение") + self.autocomplete_checkbox.setChecked(True) + + # Инициализируем QSettings с именем организации и приложения + self.settings = QSettings("SET", "DebugVarEdit_VarsSelector") + # Восстанавливаем сохранённое состояние чекбокса, если есть + checked = self.settings.value("autocomplete_enabled", True, type=bool) + self.autocomplete_checkbox.setChecked(checked) + # При изменении состояния чекбокса сохраняем его + self.autocomplete_checkbox.stateChanged.connect(self.save_checkbox_state) + self.search_input = QLineEdit() self.search_input.setPlaceholderText("Поиск по имени переменной...") - self.search_input.textChanged.connect(self.filter_tree) + self.search_input.textChanged.connect(self.on_search_text_changed) self.tree = QTreeWidget() self.tree.setHeaderLabels(["Имя переменной", "Тип"]) @@ -51,17 +64,33 @@ class VariableSelectorDialog(QDialog): self.btn_delete = QPushButton("Удалить выбранные") self.btn_delete.clicked.connect(self.on_delete_clicked) + self.completer = QCompleter() + self.completer.setCompletionMode(QCompleter.PopupCompletion) # важно! + self.completer.setCaseSensitivity(Qt.CaseInsensitive) + self.completer.setFilterMode(Qt.MatchContains) + self.completer.setWidget(self.search_input) + + + self.search_input.installEventFilter(self) + + # Создаем горизонтальный layout для "Поиск:" и чекбокса справа + search_layout = QHBoxLayout() + label_search = QLabel("Поиск:") + search_layout.addWidget(label_search, alignment=Qt.AlignLeft) + search_layout.addStretch() # чтобы чекбокс прижался вправо + search_layout.addWidget(self.autocomplete_checkbox, alignment=Qt.AlignRight) + self.completer.activated[str].connect(self.insert_completion) + layout = QVBoxLayout() - layout.addWidget(QLabel("Поиск:")) + layout.addLayout(search_layout) # заменили label и чекбокс layout.addWidget(self.search_input) layout.addWidget(self.tree) layout.addWidget(self.btn_add) - layout.addWidget(self.btn_delete) # Кнопка удаления + layout.addWidget(self.btn_delete) self.setLayout(layout) self.populate_tree() - def add_tree_item_recursively(self, parent, var): """ Рекурсивно добавляет переменную и её дочерние поля в дерево. @@ -113,7 +142,7 @@ class VariableSelectorDialog(QDialog): def filter_tree(self): text = self.search_input.text().strip().lower() - path_parts = text.split('.') if text else [] + path_parts = self.split_path(text) if text else [] def hide_all(item): item.setHidden(True) @@ -121,7 +150,7 @@ class VariableSelectorDialog(QDialog): hide_all(item.child(i)) def path_matches_search(name, search_parts): - name_parts = name.lower().split('.') + name_parts = self.split_path(name.lower()) if len(name_parts) < len(search_parts): return False for sp, np in zip(search_parts, name_parts): @@ -156,12 +185,190 @@ class VariableSelectorDialog(QDialog): return matched or matched_any_child - for i in range(self.tree.topLevelItemCount()): - item = self.tree.topLevelItem(i) - hide_all(item) - show_matching_path(item, 0) + # Если в поиске нет точки — особая логика для первого уровня + if '.' not in text and '->' not in text and text != '': + for i in range(self.tree.topLevelItemCount()): + item = self.tree.topLevelItem(i) + name = item.text(0).lower() + if text in name: + item.setHidden(False) + item.setExpanded(False) # НЕ раскрываем потомков + else: + hide_all(item) + item.setHidden(True) + else: + # Обычная логика с поиском по пути + for i in range(self.tree.topLevelItemCount()): + item = self.tree.topLevelItem(i) + hide_all(item) + show_matching_path(item, 0) + + def update_completions(self, text = None): + if text is None: + text = self.search_input.text().strip() + else: + text = text.strip() + parts = self.split_path(text) + path_parts = parts[:-1] + prefix = parts[-1].lower() if not text.endswith(('.', '>')) else '' + + # Если путь есть (например: project.adc или project.adc.), ищем внутри него + search_deep = len(path_parts) > 0 + + def find_path_items(path_parts): + items = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())] + + for part in path_parts: + part_lower = part.lower() + matched = [] + + for item in items: + # Берём последний фрагмент имени item, разделённого точками + item_name_part = self.split_path(item.text(0))[-1].lower() + + if item_name_part == part_lower: + matched.append(item) + + if not matched: + return [] + items = [] + # Собираем детей для следующего уровня поиска + for node in matched: + for i in range(node.childCount()): + items.append(node.child(i)) + + return matched + + if not search_deep: + # Без точки — ищем только в топ-уровне, фильтруя по prefix + items = [] + for i in range(self.tree.topLevelItemCount()): + item = self.tree.topLevelItem(i) + name_part = self.split_path(item.text(0))[-1].lower() + if name_part.startswith(prefix): + items.append(item) + completions = [item.text(0) for item in items] + else: + # С точкой — углубляемся по пути и показываем имена детей + if len(path_parts) == 0: + items = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())] + else: + items = find_path_items(path_parts) + + completions = [] + for item in items: + for i in range(item.childCount()): + child = item.child(i) + name_part = self.split_path(child.text(0))[-1].lower() + if prefix == '' or name_part.startswith(prefix): + completions.append(child.text(0)) + + self.completer.setModel(QStringListModel(completions)) + return completions + + + + def eventFilter(self, obj, event): + if obj == self.search_input and isinstance(event, QKeyEvent): + if event.key() == Qt.Key_Space and event.modifiers() & Qt.ControlModifier: + completions = self.update_completions() + self.completer.complete() + text = self.search_input.text().strip() + + if len(completions) == 1 and completions[0].lower() == text.lower(): + # Найдем узел с таким именем + def find_exact_item(name): + stack = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())] + while stack: + node = stack.pop() + if node.text(0).lower() == name.lower(): + return node + for i in range(node.childCount()): + stack.append(node.child(i)) + return None + + node = find_exact_item(completions[0]) + if node and node.childCount() > 0: + # Используем первую подсказку, чтобы определить нужный разделитель + completions = self.update_completions(text + '.') + suggestion = completions[0] + + # Ищем, какой символ идёт после текущего текста + separator = '.' + if suggestion.startswith(text): + rest = suggestion[len(text):] + if rest.startswith('->'): + separator = '->' + elif rest.startswith('.'): + separator = '.' + + self.search_input.setText(text + separator) + completions = self.update_completions() + self.completer.setModel(QStringListModel(completions)) + self.completer.complete() + return True + + # Иначе просто показываем подсказки + self.completer.setModel(QStringListModel(completions)) + if completions: + self.completer.complete() + return True + + return super().eventFilter(obj, event) + + # Функция для поиска узла с полным именем + def find_node_by_fullname(self, name): + stack = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())] + while stack: + node = stack.pop() + if node.text(0).lower() == name.lower(): + return node + for i in range(node.childCount()): + stack.append(node.child(i)) + return None + def insert_completion(self, text): + + node = self.find_node_by_fullname(text) + if node and node.childCount() > 0 and not (text.endswith('.') or text.endswith('->')): + # Определяем разделитель по имени первого ребёнка + child_name = node.child(0).text(0) + if child_name.startswith(text + '->'): + text += '->' + else: + text += '.' + + self.search_input.setText(text) + self.search_input.setCursorPosition(len(text)) + self.update_completions() + self.completer.complete() + else: + self.search_input.setText(text) + self.search_input.setCursorPosition(len(text)) + + def on_search_text_changed(self, text): + if self.autocomplete_checkbox.isChecked(): + completions = self.update_completions(text) + node = self.find_node_by_fullname(text) + + should_show = False + + if completions: + if len(completions) > 1: + should_show = True + elif len(completions) == 1: + single_node = self.find_node_by_fullname(completions[0]) + if single_node and single_node.childCount() > 0: + should_show = True + elif node and node.childCount() > 0 and not (text.endswith('.') or text.endswith('->')): + should_show = True + + if should_show: + self.completer.setModel(QStringListModel(completions)) + self.completer.complete() + self.filter_tree() + def on_add_clicked(self): self.selected_names = [] @@ -205,17 +412,56 @@ class VariableSelectorDialog(QDialog): self.var_map[name] = new_var # Чтобы в будущем не добавлялось повторно self.accept() - + + def on_delete_clicked(self): - # Деактивируем (удаляем из видимых) выбранные переменные - for item in self.tree.selectedItems(): - name = item.text(0) - if not name: - continue + selected_names = self._get_selected_var_names() + if not selected_names: + return + + # Обновляем var_map и all_vars + for name in selected_names: if name in self.var_map: - var = self.var_map[name] - var['show_var'] = 'false' - var['enable'] = 'false' + self.var_map[name]['show_var'] = 'false' + self.var_map[name]['enable'] = 'false' + + for v in self.all_vars: + if v['name'] == name: + v['show_var'] = 'false' + v['enable'] = 'false' + break + + # Проверка пути к XML + if not hasattr(self, 'xml_path') or not self.xml_path: + from PySide6.QtWidgets import QMessageBox + QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.") + return + + import xml.etree.ElementTree as ET + tree = ET.parse(self.xml_path) + root = tree.getroot() + if root is None: + return + + vars_section = root.find('variables') + if vars_section is None: + return + + for var_elem in vars_section.findall('var'): + name = var_elem.attrib.get('name') + if name in selected_names: + def set_text(tag, value): + el = var_elem.find(tag) + if el is None: + el = ET.SubElement(var_elem, tag) + el.text = value + set_text('show_var', 'false') + set_text('enable', 'false') + + ET.indent(tree, space=" ", level=0) + tree.write(self.xml_path, encoding='utf-8', xml_declaration=True) + + self.populate_tree() self.accept() @@ -231,16 +477,11 @@ class VariableSelectorDialog(QDialog): super().keyPressEvent(event) def delete_selected_vars(self): - # Деактивируем (удаляем из видимых) выбранные переменные - for item in self.tree.selectedItems(): - name = item.text(0) - if not name: - continue - if name in self.var_map: - var = self.var_map[name] - var['show_var'] = 'false' - var['enable'] = 'false' + selected_names = self._get_selected_var_names() + if not selected_names: + return + # Проверка пути к XML if not hasattr(self, 'xml_path') or not self.xml_path: from PySide6.QtWidgets import QMessageBox QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно удалить переменные.") @@ -249,31 +490,39 @@ class VariableSelectorDialog(QDialog): import xml.etree.ElementTree as ET tree = ET.parse(self.xml_path) root = tree.getroot() - if root is None: return - + vars_section = root.find('variables') if vars_section is None: - return # Нет секции variables — ничего удалять - - selected_names = [item.text(0) for item in self.tree.selectedItems() if item.text(0)] + return removed_any = False - for var_elem in vars_section.findall('var'): + for var_elem in list(vars_section.findall('var')): name = var_elem.attrib.get('name') if name in selected_names: vars_section.remove(var_elem) removed_any = True - if name in self.var_map: - del self.var_map[name] - # Удаляем элементы из списка на месте - self.all_vars[:] = [v for v in self.all_vars if v['name'] != name] + self.var_map.pop(name, None) + # Удаляем из all_vars (глобально) + self.all_vars[:] = [v for v in self.all_vars if v['name'] not in selected_names] if removed_any: ET.indent(tree, space=" ", level=0) tree.write(self.xml_path, encoding='utf-8', xml_declaration=True) + self.populate_tree() + self.filter_tree() - self.populate_tree() \ No newline at end of file + def _get_selected_var_names(self): + return [item.text(0) for item in self.tree.selectedItems() if item.text(0)] + + + def save_checkbox_state(self): + self.settings.setValue("autocomplete_enabled", self.autocomplete_checkbox.isChecked()) + + + def split_path(self, path): + # Разбиваем по точке или по -> (учитываем, что -> длиной 2 символа) + return re.split(r'\.|->', path) \ No newline at end of file diff --git a/Src/VariableTable.py b/Src/VariableTable.py new file mode 100644 index 0000000..987d4e7 --- /dev/null +++ b/Src/VariableTable.py @@ -0,0 +1,184 @@ +from PySide6.QtWidgets import ( + QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QCompleter, + QAbstractItemView, QHeaderView +) +from PySide6.QtCore import Qt +from enum import IntEnum +from generateVars import type_map + +class rows(IntEnum): + No = 0 + include = 1 + name = 2 + type = 3 + pt_type = 4 + iq_type = 5 + ret_type = 6 + short_name = 7 + + +class VariableTableWidget(QTableWidget): + def __init__(self, parent=None): + super().__init__(0, 8, parent) + # Таблица переменных + self.setHorizontalHeaderLabels([ + '№', # новый столбец + 'En', + 'Name', + 'Origin Type', + 'Pointer Type', + 'IQ Type', + 'Return Type', + 'Short Name' + ]) + self.setEditTriggers(QAbstractItemView.AllEditTriggers) + + + self.type_options = list(dict.fromkeys(type_map.values())) + self.display_type_options = [t.replace('pt_', '') for t in self.type_options] + self.iq_types = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)] + + header = self.horizontalHeader() + # Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину + + for col in range(self.columnCount()): + if col == self.columnCount() - 1: + header.setSectionResizeMode(col, QHeaderView.Stretch) + else: + header.setSectionResizeMode(col, QHeaderView.Interactive) + + parent_widget = self.parentWidget() + # Сделаем колонки с номерами фиксированной ширины + self.setColumnWidth(rows.No, 30) + self.setColumnWidth(rows.include, 30) + self.setColumnWidth(rows.pt_type, 85) + self.setColumnWidth(rows.iq_type, 85) + self.setColumnWidth(rows.ret_type, 85) + + self.setColumnWidth(rows.name, 300) + self.setColumnWidth(rows.type, 100) + self._resizing = False + self.horizontalHeader().sectionResized.connect(self.on_section_resized) + + + def populate(self, vars_list, structs, on_change_callback): + 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 vars_list if v.get('show_var', 'false') == 'true'] + self.setRowCount(len(filtered_vars)) + self.verticalHeader().setVisible(False) + + for row, var in enumerate(filtered_vars): + # № + no_item = QTableWidgetItem(str(row)) + no_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + self.setItem(row, rows.No, no_item) + + # Enable + cb = QCheckBox() + cb.setChecked(var.get('enable', 'false') == 'true') + cb.stateChanged.connect(on_change_callback) + self.setCellWidget(row, rows.include, cb) + + # Name + name_edit = QLineEdit(var['name']) + if var['type'] in structs: + completer = QCompleter(structs[var['type']].keys()) + completer.setCaseSensitivity(Qt.CaseInsensitive) + name_edit.setCompleter(completer) + name_edit.textChanged.connect(on_change_callback) + self.setCellWidget(row, rows.name, name_edit) + + # Origin Type (readonly) + origin_item = QTableWidgetItem(var.get('type', '')) + origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + self.setItem(row, rows.type, origin_item) + + # pt_type + pt_combo = QComboBox() + pt_combo.addItems(self.display_type_options) + value = var['pt_type'].replace('pt_', '') + if value not in self.display_type_options: + pt_combo.addItem(value) + pt_combo.setCurrentText(value) + pt_combo.currentTextChanged.connect(on_change_callback) + self.setCellWidget(row, rows.pt_type, pt_combo) + + # iq_type + iq_combo = QComboBox() + iq_combo.addItems(self.iq_types) + value = var['iq_type'].replace('t_', '') + if value not in self.iq_types: + iq_combo.addItem(value) + iq_combo.setCurrentText(value) + iq_combo.currentTextChanged.connect(on_change_callback) + self.setCellWidget(row, rows.iq_type, iq_combo) + + # return_type + ret_combo = QComboBox() + ret_combo.addItems(self.iq_types) + ret_combo.setCurrentText(var.get('return_type', '')) + ret_combo.currentTextChanged.connect(on_change_callback) + self.setCellWidget(row, rows.ret_type, ret_combo) + + # short_name + short_name_edit = QLineEdit(var.get('shortname', var['name'])) + short_name_edit.textChanged.connect(on_change_callback) + self.setCellWidget(row, rows.short_name, short_name_edit) + + + + def read_data(self): + result = [] + for row in range(self.rowCount()): + cb = self.cellWidget(row, rows.include) + name = self.cellWidget(row, rows.name).text() + pt = self.cellWidget(row, rows.pt_type).currentText() + iq = self.cellWidget(row, rows.iq_type).currentText() + ret = self.cellWidget(row, rows.ret_type).currentText() + shortname = self.cellWidget(row, rows.short_name).text() + origin_type = self.item(row, rows.type).text() + + result.append({ + 'show_var': True, + 'enable': cb.isChecked(), + 'name': name, + 'pt_type': f'pt_{pt}', + 'iq_type': iq, + 'return_type': ret, + 'shortname': shortname, + 'type': origin_type, + }) + return result + + def on_section_resized(self, logicalIndex, oldSize, newSize): + if self._resizing: + return # предотвращаем рекурсию + + min_width = 50 + delta = newSize - oldSize + right_index = logicalIndex + 1 + + if right_index >= self.columnCount(): + # Если правая колока - нет соседа, ограничиваем минимальную ширину + if newSize < min_width: + self._resizing = True + self.setColumnWidth(logicalIndex, min_width) + self._resizing = False + return + + self._resizing = True + try: + right_width = self.columnWidth(right_index) + new_right_width = right_width - delta + + # Если соседняя колонка станет уже минимальной - подкорректируем левую + if new_right_width < min_width: + new_right_width = min_width + newSize = oldSize + (right_width - min_width) + self.setColumnWidth(logicalIndex, newSize) + + self.setColumnWidth(right_index, new_right_width) + finally: + self._resizing = False diff --git a/Src/__pycache__/VariableSelector.cpython-313.pyc b/Src/__pycache__/VariableSelector.cpython-313.pyc new file mode 100644 index 0000000..37ff5cf Binary files /dev/null and b/Src/__pycache__/VariableSelector.cpython-313.pyc differ diff --git a/Src/__pycache__/VariableTable.cpython-313.pyc b/Src/__pycache__/VariableTable.cpython-313.pyc new file mode 100644 index 0000000..4dc94e5 Binary files /dev/null and b/Src/__pycache__/VariableTable.cpython-313.pyc differ diff --git a/Src/__pycache__/generateVars.cpython-313.pyc b/Src/__pycache__/generateVars.cpython-313.pyc new file mode 100644 index 0000000..1ee5e17 Binary files /dev/null and b/Src/__pycache__/generateVars.cpython-313.pyc differ diff --git a/Src/__pycache__/parseMakefile.cpython-313.pyc b/Src/__pycache__/parseMakefile.cpython-313.pyc new file mode 100644 index 0000000..da23168 Binary files /dev/null and b/Src/__pycache__/parseMakefile.cpython-313.pyc differ diff --git a/Src/__pycache__/scanVars.cpython-313.pyc b/Src/__pycache__/scanVars.cpython-313.pyc new file mode 100644 index 0000000..d19ea13 Binary files /dev/null and b/Src/__pycache__/scanVars.cpython-313.pyc differ diff --git a/Src/__pycache__/setupVars.cpython-313.pyc b/Src/__pycache__/setupVars.cpython-313.pyc new file mode 100644 index 0000000..5a45c6e Binary files /dev/null and b/Src/__pycache__/setupVars.cpython-313.pyc differ diff --git a/Src/generateVars.py b/Src/generateVars.py index 78865f1..cbb941e 100644 --- a/Src/generateVars.py +++ b/Src/generateVars.py @@ -330,12 +330,13 @@ def generate_vars_file(proj_path, xml_path, output_dir): # Дополнительные поля, например комментарий comment = info.get("comment", "") + short_name = info.get("shortname", f'"{vname}"') if pt_type not in ('pt_struct', 'pt_union'): formated_name = f'"{vname}"' # Добавим комментарий после записи, если он есть comment_str = f' // {comment}' if comment else '' - line = f'{{(char *)&{vname:<41} , {pt_type:<21} , {iq_type:<21} , {formated_name:<42}}}, \\{comment_str}' + line = f'{{(char *)&{vname:<41} , {pt_type:<21} , {iq_type:<21} , {short_name:<42}}}, \\{comment_str}' new_debug_vars[vname] = line else: diff --git a/Src/parseMakefile.py b/Src/parseMakefile.py index 6bbcba2..e2e17e0 100644 --- a/Src/parseMakefile.py +++ b/Src/parseMakefile.py @@ -7,11 +7,14 @@ def strip_single_line_comments(code): return re.sub(r'//.*?$', '', code, flags=re.MULTILINE) def read_file_try_encodings(filepath): + if not os.path.isfile(filepath): + # Файл не существует — просто вернуть пустую строку или None + return "", None for enc in ['utf-8', 'cp1251']: try: with open(filepath, 'r', encoding=enc) as f: content = f.read() - content = strip_single_line_comments(content) # <=== ВАЖНО + content = strip_single_line_comments(content) return content, enc except UnicodeDecodeError: continue @@ -38,6 +41,8 @@ def find_all_includes_recursive(c_files, include_dirs, processed_files=None): processed_files.add(norm_path) content, _ = read_file_try_encodings(cfile) + if content is None: + continue includes = include_pattern.findall(content) for inc in includes: # Ищем полный путь к include-файлу в include_dirs @@ -61,9 +66,9 @@ def find_all_includes_recursive(c_files, include_dirs, processed_files=None): return include_files -def parse_makefile(makefile_path): +def parse_makefile(makefile_path, proj_path): makefile_dir = os.path.dirname(makefile_path) - project_root = os.path.dirname(makefile_dir) # поднялись из Debug + project_root = proj_path with open(makefile_path, 'r', encoding='utf-8') as f: lines = f.readlines() @@ -115,6 +120,8 @@ def parse_makefile(makefile_path): continue if "v120" in obj_path: continue + if "v100" in obj_path: + continue if obj_path.startswith("Debug\\") or obj_path.startswith("Debug/"): rel_path = obj_path.replace("Debug\\", "Src\\").replace("Debug/", "Src/") @@ -129,6 +136,10 @@ def parse_makefile(makefile_path): else: c_path = abs_path + # Проверяем существование файла, если нет — пропускаем + if not os.path.isfile(c_path): + continue + # Сохраняем только .c файлы if c_path.lower().endswith(".c"): c_files.append(c_path) diff --git a/Src/scanVars.py b/Src/scanVars.py index 66ea92c..ff1fbe6 100644 --- a/Src/scanVars.py +++ b/Src/scanVars.py @@ -812,7 +812,7 @@ Usage example: print(f"Error: Makefile path '{makefile_path}' does not exist.") sys.exit(1) - c_files, h_files, include_dirs = parse_makefile(makefile_path) + c_files, h_files, include_dirs = parse_makefile(makefile_path, proj_path) vars, includes, externs = analyze_variables_across_files(c_files, h_files, include_dirs) typedefs, structs = analyze_typedefs_and_structs_across_files(c_files, include_dirs) @@ -855,7 +855,7 @@ def run_scan(proj_path, makefile_path, output_xml, verbose=2): if not os.path.isfile(makefile_path): raise FileNotFoundError(f"Makefile path '{makefile_path}' does not exist.") - c_files, h_files, include_dirs = parse_makefile(makefile_path) + c_files, h_files, include_dirs = parse_makefile(makefile_path, proj_path) vars, includes, externs = analyze_variables_across_files(c_files, h_files, include_dirs) typedefs, structs = analyze_typedefs_and_structs_across_files(c_files, include_dirs) diff --git a/Src/setupVars.py b/Src/setupVars.py index ae20700..3eabb48 100644 --- a/Src/setupVars.py +++ b/Src/setupVars.py @@ -63,6 +63,21 @@ def parse_vars(filename, typedef_map=None): iq_type = var.findtext('iq_type') if not iq_type: iq_type = get_iq_define(var_type) + # Записываем iq_type в XML + iq_type_elem = var.find('iq_type') + if iq_type_elem is None: + iq_type_elem = ET.SubElement(var, 'iq_type') + iq_type_elem.text = iq_type + + # Вычисляем pt_type и iq_type + pt_type = var.findtext('pt_type') + if not pt_type: + pt_type = map_type_to_pt(var_type, name, typedef_map) + # Записываем pt_type в XML + pt_type_elem = var.find('pt_type') + if pt_type_elem is None: + pt_type_elem = ET.SubElement(var, 'pt_type') + pt_type_elem.text = pt_type vars_list.append({ 'name': name, @@ -78,6 +93,10 @@ def parse_vars(filename, typedef_map=None): 'static': var.findtext('static', 'false') == 'true', }) + ET.indent(tree, space=" ", level=0) + # Сохраняем изменения в XML-файл + tree.write(filename, encoding='utf-8', xml_declaration=True) + return vars_list @@ -159,6 +178,9 @@ def safe_parse_xml(xml_path): except Exception as e: print(f"Неожиданная ошибка при чтении XML файла '{xml_path}': {e}") return None, None + + + def expand_struct_recursively(prefix, type_str, structs, typedefs, var_attrs, depth=0): if depth > 10: return [] @@ -178,7 +200,15 @@ def expand_struct_recursively(prefix, type_str, structs, typedefs, var_attrs, de if field_name == 'type': continue - full_name = f"{prefix}.{field_name}" + # Определяем разделитель между prefix и полем + if prefix.endswith('*'): + separator = '->' + # Для красоты можно убрать пробелы у указателя + # например, если prefix="ptr*" -> "ptr->field" + full_name = f"{prefix[:-1]}{separator}{field_name}" + else: + separator = '.' + full_name = f"{prefix}{separator}{field_name}" if isinstance(field_value, dict): # Если вложенная структура — берем её имя типа из поля 'type' или пустую строку @@ -187,10 +217,14 @@ def expand_struct_recursively(prefix, type_str, structs, typedefs, var_attrs, de 'name': full_name, 'type': type_name, 'pt_type': '', + 'iq_type': '', + 'return_type': '', 'file': var_attrs.get('file'), 'extern': var_attrs.get('extern'), 'static': var_attrs.get('static'), } + if '*' in type_name and not full_name.endswith('*'): + full_name += '*' # Рекурсивно раскрываем вложенные поля subchildren = expand_struct_recursively(full_name, field_value, structs, typedefs, var_attrs, depth + 1) if subchildren: @@ -205,6 +239,8 @@ def expand_struct_recursively(prefix, type_str, structs, typedefs, var_attrs, de 'name': full_name, 'type': field_value, 'pt_type': '', + 'iq_type': '', + 'return_type': '', 'file': var_attrs.get('file'), 'extern': var_attrs.get('extern'), 'static': var_attrs.get('static'), @@ -215,6 +251,7 @@ def expand_struct_recursively(prefix, type_str, structs, typedefs, var_attrs, de return children + def expand_vars(vars_list, structs, typedefs): """ Раскрывает структуры и массивы структур в деревья. @@ -228,6 +265,11 @@ def expand_vars(vars_list, structs, typedefs): fields = structs.get(base_type) + if pt_type.startswith('pt_ptr_') and isinstance(fields, dict): + new_var = var.copy() + new_var['children'] = expand_struct_recursively(var['name']+'*', raw_type, structs, typedefs, var) + expanded.append(new_var) + if pt_type.startswith('pt_arr_') and isinstance(fields, dict): new_var = var.copy() new_var['children'] = expand_struct_recursively(var['name'], raw_type, structs, typedefs, var) diff --git a/Src/setupVars_GUI.py b/Src/setupVars_GUI.py index 5a4fb79..d13404e 100644 --- a/Src/setupVars_GUI.py +++ b/Src/setupVars_GUI.py @@ -12,6 +12,7 @@ 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, @@ -20,19 +21,9 @@ from PySide6.QtWidgets import ( QDialog, QTreeWidget, QTreeWidgetItem, QSizePolicy ) from PySide6.QtGui import QTextCursor, QKeyEvent -from PySide6.QtCore import Qt, QProcess, QObject, Signal, QTimer +from PySide6.QtCore import Qt, QProcess, QObject, Signal, QSettings -class rows(IntEnum): - No = 0 - include = 1 - name = 2 - type = 3 - pt_type = 4 - iq_type = 5 - ret_type = 6 - short_name = 7 - class EmittingStream(QObject): text_written = Signal(str) @@ -54,11 +45,12 @@ class EmittingStream(QObject): self._buffer = "" -class ProcessOutputWindowDummy(QWidget): +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() @@ -75,7 +67,7 @@ class ProcessOutputWindowDummy(QWidget): def __handle_done(self): if self._on_done_callback: self._on_done_callback() - self.close() + self.accept() # закрыть диалог def append_text(self, text): cursor = self.output_edit.textCursor() @@ -161,20 +153,6 @@ class VarEditor(QWidget): self.btn_update_vars = QPushButton("Обновить данные о переменных") self.btn_update_vars.clicked.connect(self.update_vars_data) - # Таблица переменных - self.table = QTableWidget(len(self.vars_list), 8) - self.table.setHorizontalHeaderLabels([ - '№', # новый столбец - 'En', - '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) @@ -183,7 +161,8 @@ class VarEditor(QWidget): self.btn_add_vars = QPushButton("Add Variables") self.btn_add_vars.clicked.connect(self.__open_variable_selector) - + # Таблица + self.table = VariableTableWidget() # Основной layout layout = QVBoxLayout() layout.addLayout(xml_layout) @@ -195,68 +174,9 @@ class VarEditor(QWidget): layout.addLayout(source_output_layout) layout.addWidget(btn_save) - - header = self.table.horizontalHeader() - # Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину - - for col in range(self.table.columnCount()): - if col == self.table.columnCount() - 1: - header.setSectionResizeMode(col, QHeaderView.Stretch) - else: - header.setSectionResizeMode(col, QHeaderView.Interactive) - - parent_widget = self.table.parentWidget() - if parent_widget: - w = parent_widget.width() - h = parent_widget.height() - viewport_width = self.table.viewport().width() - # Сделаем колонки с номерами фиксированной ширины - self.table.setColumnWidth(rows.No, 30) - self.table.setColumnWidth(rows.include, 30) - self.table.setColumnWidth(rows.pt_type, 85) - self.table.setColumnWidth(rows.iq_type, 85) - self.table.setColumnWidth(rows.ret_type, 85) - - self.table.setColumnWidth(rows.name, 300) - self.table.setColumnWidth(rows.type, 100) - - self.table.horizontalHeader().sectionResized.connect(self.on_section_resized) - self.setLayout(layout) - def on_section_resized(self, logicalIndex, oldSize, newSize): - if self._resizing: - return # предотвращаем рекурсию - - min_width = 50 - delta = newSize - oldSize - right_index = logicalIndex + 1 - - if right_index >= self.table.columnCount(): - # Если правая колока - нет соседа, ограничиваем минимальную ширину - if newSize < min_width: - self._resizing = True - self.table.setColumnWidth(logicalIndex, min_width) - self._resizing = False - return - - self._resizing = True - try: - right_width = self.table.columnWidth(right_index) - new_right_width = right_width - delta - - # Если соседняя колонка станет уже минимальной - подкорректируем левую - if new_right_width < min_width: - new_right_width = min_width - newSize = oldSize + (right_width - min_width) - self.table.setColumnWidth(logicalIndex, newSize) - - self.table.setColumnWidth(right_index, new_right_width) - finally: - self._resizing = False - - def get_xml_path(self): xml_path = self.xml_output_edit.text().strip() @@ -416,18 +336,17 @@ class VarEditor(QWidget): else: self.makefile_path = None - if not self.structs_path: - # --- 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 + # --- 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() + self.table.populate(self.vars_list, self.structs, self.write_to_xml) except Exception as e: QMessageBox.warning(self, "Ошибка", f"Ошибка при чтении XML:\n{e}") @@ -528,7 +447,7 @@ class VarEditor(QWidget): v['show_var'] = 'false' break - self.update_table() + self.table.populate(self.vars_list, self.structs, self.write_to_xml) def __open_variable_selector(self): @@ -542,102 +461,6 @@ class VarEditor(QWidget): self.update() - - 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)) - self.table.verticalHeader().setVisible(False) - - for row, var in enumerate(filtered_vars): - # Добавляем номер строки в колонку No (0) - no_item = QTableWidgetItem(str(row)) - no_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) # readonly - self.table.setItem(row, rows.No, no_item) - - 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() @@ -682,7 +505,7 @@ class VarEditor(QWidget): } # Читаем переменные из таблицы (активные/изменённые) - table_vars = {v['name']: v for v in self.read_table()} + 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} @@ -707,8 +530,11 @@ class VarEditor(QWidget): 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')) + 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', '') diff --git a/vars.xml b/vars.xml index 479e94b..3a13a7c 100644 --- a/vars.xml +++ b/vars.xml @@ -1,5 +1,5 @@ - - + + false @@ -14,7 +14,7 @@ false - false + true true ADC0startAddr pt_int16 @@ -407,7 +407,7 @@ char[8][16] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -863,7 +863,7 @@ int[12] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -875,7 +875,7 @@ int[12] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -959,7 +959,7 @@ int[12] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -971,7 +971,7 @@ int[12] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -1151,7 +1151,7 @@ char[4] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -1163,7 +1163,7 @@ char Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -1175,7 +1175,7 @@ char Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -1187,7 +1187,7 @@ char[5] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -1199,7 +1199,7 @@ char[4][8] Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -1283,7 +1283,7 @@ unsigned int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myXilinx/x_serial_bus.c false - true + True false @@ -1319,7 +1319,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/message_modbus.c false - true + True false @@ -1343,7 +1343,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/message_modbus.c false - true + True false @@ -1367,7 +1367,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -1379,7 +1379,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -1391,7 +1391,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -1403,7 +1403,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/init_protect_levels.c false - true + True false @@ -1451,7 +1451,7 @@ DQ_TO_ALPHABETA Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/v_pwm24.c false - true + True false @@ -2039,7 +2039,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -2279,7 +2279,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c false - true + True false @@ -2375,7 +2375,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/PWMTMSHandle.c false - true + True false @@ -2387,7 +2387,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/PWMTMSHandle.c false - true + True false @@ -2531,7 +2531,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -2543,7 +2543,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -2555,7 +2555,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -2567,7 +2567,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -2579,7 +2579,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -2591,7 +2591,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -2603,7 +2603,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -2615,7 +2615,7 @@ int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/modbus_read_table.c false - true + True false @@ -3311,7 +3311,7 @@ unsigned int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/main22220.c false - true + True false @@ -3323,7 +3323,7 @@ unsigned int Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/main/main22220.c false - true + True false @@ -3577,18 +3577,6 @@ false false - - true - true - Bender.KOhms - pt_uint16 - iq_none - iq_none - unsigned int - Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/DebugTools/Src/myLibs/bender.c - false - false - Src/main/vector.h @@ -4697,4 +4685,4 @@ Src/main/adc_tools.c - + \ No newline at end of file