From d3f1e824fa290154a990fef43c1ef8612814192d Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Fri, 11 Jul 2025 10:48:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE.=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=BC=20=D0=B7=D0=B0=D0=BF=D1=83?= =?UTF-8?q?=D1=81=D0=BA=D0=B5=20=D0=92=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D0=B5=D1=87=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B0=D0=B5=D1=82,=20=D0=BD=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D1=81=D0=BA=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BE=D1=82=D0=BD=D0=BE=D1=81=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=20=D1=88=D1=83=D1=81=D1=82=D1=80=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/DebugVarEdit_GUI.py | 1 + Src/VariableSelector.py | 316 ++++++++++++++++++++-------------------- Src/setupVars.py | 45 ++++-- 3 files changed, 190 insertions(+), 172 deletions(-) diff --git a/Src/DebugVarEdit_GUI.py b/Src/DebugVarEdit_GUI.py index 030e3d7..aad75e7 100644 --- a/Src/DebugVarEdit_GUI.py +++ b/Src/DebugVarEdit_GUI.py @@ -16,6 +16,7 @@ from VariableTable import VariableTableWidget, rows from scanVarGUI import ProcessOutputWindowDummy import scanVars import myXML +import time from PySide2.QtWidgets import ( QApplication, QWidget, QTableWidget, QTableWidgetItem, diff --git a/Src/VariableSelector.py b/Src/VariableSelector.py index b7a4431..4647612 100644 --- a/Src/VariableSelector.py +++ b/Src/VariableSelector.py @@ -7,6 +7,7 @@ from PySide2.QtGui import QKeySequence, QKeyEvent from PySide2.QtCore import Qt, QStringListModel, QSettings import setupVars import myXML +import time array_re = re.compile(r'^(\w+)\[(\d+)\]$') @@ -18,6 +19,7 @@ class VariableSelectorDialog(QDialog): self.setAttribute(Qt.WA_DeleteOnClose) self.resize(600, 500) self.selected_names = [] + self._bckspc_pressed = False # флаг подавления добавления разделителя self.all_vars = all_vars self.structs = structs @@ -153,88 +155,56 @@ class VariableSelectorDialog(QDialog): self.add_tree_item_recursively(item, child) - def populate_tree(self): + def populate_tree(self, vars_list=None): + if vars_list is None: + vars_list = self.expanded_vars self.tree.clear() self.node_index.clear() - - for var in self.expanded_vars: + for var in vars_list: self.add_tree_item_recursively(None, var) header = self.tree.header() header.setSectionResizeMode(QHeaderView.Interactive) # вручную можно менять self.tree.setColumnWidth(0, 400) self.tree.resizeColumnToContents(1) - """ header.setSectionResizeMode(0, QHeaderView.Stretch) - header.setSectionResizeMode(1, QHeaderView.ResizeToContents) """ + + def expand_to_level(self, item, level, current_level=0): + """ + Рекурсивно раскрывает узлы до заданного уровня. + """ + if current_level < level: + item.setExpanded(True) + else: + item.setExpanded(False) + + for i in range(item.childCount()): + self.expand_to_level(item.child(i), level, current_level + 1) def filter_tree(self): text = self.search_input.text().strip().lower() - path_parts = self.split_path(text) if text else [] + path_parts = text.split('.') if text else [] + filtered_vars = filter_vars(self.expanded_vars, path_parts) - def hide_all(item): - item.setHidden(True) - for i in range(item.childCount()): - hide_all(item.child(i)) + # Сначала перерисовываем дерево + self.populate_tree(filtered_vars) - def path_matches_search(name, search_parts): - 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): - if not np.startswith(sp): - return False - return True + # Теперь node_index уже пересоздан — можно работать + expand_level = len(path_parts) - 1 if path_parts else 0 + for i in range(self.tree.topLevelItemCount()): + item = self.tree.topLevelItem(i) + self.expand_to_level(item, expand_level) - def show_matching_path(item, level=0): - name = item.text(0).lower() + # Раскрываем путь до точного совпадения + if path_parts: + fullname = '.'.join(path_parts) + node = self.node_index.get(fullname.lower()) + if node: + parent = node.parent() + while parent: + parent.setExpanded(True) + parent = parent.parent() - # По умолчанию не совпадает - matched = False - - # Проверяем путь - if not path_parts: - matched = True - elif path_matches_search(name, path_parts[:level+1]): - matched = True - - # Исключаем "плоские" элементы на верхнем уровне при глубоком поиске - if level == 0 and item.childCount() == 0 and len(path_parts) > 1: - matched = False - - item.setHidden(not matched) - - # Раскрываем узел, если он соответствует и ещё есть путь вниз - if matched and level < len(path_parts) - 1: - item.setExpanded(True) - else: - item.setExpanded(False) - - matched_any_child = False - for i in range(item.childCount()): - child = item.child(i) - if show_matching_path(child, level + 1): - matched_any_child = True - - return matched or matched_any_child - - - # Если в поиске нет точки — особая логика для первого уровня - 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 find_node_by_path(self, root_vars, path_list): current_level = root_vars @@ -249,13 +219,13 @@ class VariableSelectorDialog(QDialog): return None current_level = node.get('children', []) return node - + 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] if parts else [] prefix = parts[-1].lower() if parts else '' @@ -264,29 +234,23 @@ class VariableSelectorDialog(QDialog): completions = [] - def find_path_items(parts): - items = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())] - for part in parts: - part_lower = part.lower() - matched = [] - for item in items: - name_parts = self.split_path(item.text(0).lower()) - if name_parts and name_parts[-1] == 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 + def find_exact_node(parts): + # Ищем точный узел по полному пути, используя node_index + # Постепенно собираем fullname из parts + if not parts: + return None + fullname = parts[0] + for p in parts[1:]: + fullname += '.' + p + return self.node_index.get(fullname.lower()) if is_index_suggestion: - # Предлагаем индексы (ищем всех детей с форматом foo[0], foo[1], ...) - base_text = text[:-1] # убираем '[', получаем, например, 'foo' + base_text = text[:-1] # убираем '[' parent_node = self.find_node_by_fullname(base_text) if not parent_node: - parent_node = self.find_node_by_fullname(base_text.rstrip('0123456789[]')) + # если base_text может содержать индекс типа foo[12], попробуем очистить + base_text_clean = re.sub(r'\[\d+\]$', '', base_text) + parent_node = self.find_node_by_fullname(base_text_clean) if parent_node: seen = set() for i in range(parent_node.childCount()): @@ -300,11 +264,10 @@ class VariableSelectorDialog(QDialog): return completions if ends_with_sep: - # Завершён путь (например: project.adc[0].), показываем детей + # Путь завершен, показываем детей узла node = self.find_node_by_fullname(text[:-1]) if node: - for i in range(node.childCount()): - completions.append(node.child(i).text(0)) + completions.extend(node.child(i).text(0) for i in range(node.childCount())) elif not path_parts: # Первый уровень — по вхождению for i in range(self.tree.topLevelItemCount()): @@ -313,24 +276,28 @@ class VariableSelectorDialog(QDialog): if prefix in name: completions.append(item.text(0)) else: - # Углубляемся: на последнем уровне используем startswith(prefix) - matched_items = find_path_items(path_parts) - for item in matched_items: - for i in range(item.childCount()): - child = item.child(i) + node = find_exact_node(path_parts) + if node: + for i in range(node.childCount()): + child = node.child(i) name = child.text(0) - name_parts = self.split_path(name) + # Оптимизируем split_path - кэширование + name_parts = child.data(0, Qt.UserRole + 10) + if name_parts is None: + name_parts = self.split_path(name) + child.setData(0, Qt.UserRole + 10, name_parts) if not name_parts: continue last_part = name_parts[-1].lower() if prefix == '' or last_part.startswith(prefix): completions.append(name) - - self.completer.setModel(QStringListModel(completions)) + + self.completer.complete() return completions + # Функция для поиска узла с полным именем def find_node_by_fullname(self, name): return self.node_index.get(name.lower()) @@ -348,86 +315,82 @@ class VariableSelectorDialog(QDialog): text += '[' # для массивов else: text += '.' # fallback + + if not self._bckspc_pressed: + self.search_input.setText(text) + self.search_input.setCursorPosition(len(text)) - self.search_input.setText(text) - self.search_input.setCursorPosition(len(text)) - self.update_completions() - self.completer.complete() + self.run_completions(text) else: self.search_input.setText(text) self.search_input.setCursorPosition(len(text)) - - 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() + self.run_completions(text) return True + if event.key() == Qt.Key_Backspace: + self._bckspc_pressed = True + else: + self._bckspc_pressed = False return super().eventFilter(obj, event) - def on_search_text_changed(self, text): - if self.autocomplete_checkbox.isChecked(): - completions = self.update_completions(text) - node = self.find_node_by_fullname(text) + def run_completions(self, text): + completions = self.update_completions(text) - should_show = False + 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 - 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 + node = find_exact_item(completions[0]) + if node and node.childCount() > 0: + # Используем первую подсказку, чтобы определить нужный разделитель + completions = self.update_completions(text + '.') + suggestion = completions[0] - if should_show: + # Ищем, какой символ идёт после текущего текста + separator = '.' + if suggestion.startswith(text): + rest = suggestion[len(text):] + if rest.startswith(text + '->'): + separator += '->' + elif rest.startswith(text + '.'): + separator += '.' + elif '[' in rest: + separator += '[' # для массивов + else: + separator += '.' # fallback + + if not self._bckspc_pressed: + self.search_input.setText(text + separator) + completions = self.update_completions(text) self.completer.setModel(QStringListModel(completions)) self.completer.complete() + return True + + # Иначе просто показываем подсказки + self.completer.setModel(QStringListModel(completions)) + if completions: + self.completer.complete() + return True + + def on_search_text_changed(self, text): self.filter_tree() + if text == None: + text = self.search_input.text().strip() + if self.autocomplete_checkbox.isChecked(): + self.run_completions(text) def on_add_clicked(self): self.selected_names = [] @@ -630,3 +593,36 @@ class VariableSelectorDialog(QDialog): tokens.append(token) return tokens + +def filter_vars(vars_list, path_parts): + """Рекурсивно фильтруем vars_list по path_parts и возвращаем только подходящие.""" + filtered = [] + + def matches_path(name, search_parts): + name_parts = name.lower().split('.') + if len(name_parts) < len(search_parts): + return False + for sp, np in zip(search_parts, name_parts): + if not np.startswith(sp): + return False + return True + + for var in vars_list: + fullname = var.get('fullname', var['name']) # желательно иметь полное имя + # Если фильтра нет — берем всё + if not path_parts or matches_path(fullname, path_parts): + # Копируем узел с рекурсией по детям + new_var = var.copy() + if 'children' in var: + new_var['children'] = filter_vars(var['children'], path_parts) + filtered.append(new_var) + else: + # Но даже если этот узел не подходит, может подойти его потомок + if 'children' in var: + child_filtered = filter_vars(var['children'], path_parts) + if child_filtered: + new_var = var.copy() + new_var['children'] = child_filtered + filtered.append(new_var) + + return filtered diff --git a/Src/setupVars.py b/Src/setupVars.py index 542f934..f3fb7ac 100644 --- a/Src/setupVars.py +++ b/Src/setupVars.py @@ -289,18 +289,39 @@ def expand_struct_recursively(prefix, type_str, structs, typedefs, var_attrs, de if "(" in field_type_str and "*" in field_type_str and ")" in field_type_str: continue - # Обычное поле (не массив, не функция) - child = { - 'name': full_name, - 'type': field_type_str, - 'pt_type': '', - 'iq_type': '', - 'return_type': '', - 'file': var_attrs.get('file'), - 'extern': var_attrs.get('extern'), - 'static': var_attrs.get('static'), - } - children.append(child) + # Проверим, является ли тип структурой (по имени) + clean_type = scanVars.strip_ptr_and_array(field_type_str) + struct_fields = structs.get(clean_type) + + if isinstance(struct_fields, dict): + # Это одиночная структура — раскрываем рекурсивно + sub_items = expand_struct_recursively(full_name, field_type_str, structs, typedefs, var_attrs, depth + 1) + child = { + 'name': full_name, + 'type': field_type_str, + 'pt_type': '', + 'iq_type': '', + 'return_type': '', + 'file': var_attrs.get('file'), + 'extern': var_attrs.get('extern'), + 'static': var_attrs.get('static'), + } + if sub_items: + child['children'] = sub_items + children.append(child) + else: + # Обычное поле (int, float, etc.) + child = { + 'name': full_name, + 'type': field_type_str, + 'pt_type': '', + 'iq_type': '', + 'return_type': '', + 'file': var_attrs.get('file'), + 'extern': var_attrs.get('extern'), + 'static': var_attrs.get('static'), + } + children.append(child) continue # Если поле — dict без 'type' или со сложной структурой, обрабатываем как вложенную структуру