# Поместите этот код перед классом VariableSelectorDialog import re from PySide2.QtWidgets import ( QWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QLineEdit, QHeaderView, QCompleter ) from PySide2.QtGui import QKeyEvent from PySide2.QtCore import Qt, QStringListModel # Вспомогательные функции, которые теперь будут использоваться виджетом def split_path(path): """ Разбивает путь на компоненты: - 'foo[2].bar[1]->baz' → ['foo', [2]', 'bar', '[1]' 'baz'] """ tokens = [] token = '' i = 0 while i < len(path): c = path[i] # Разделители: '->' и '.' if c == '-' and path[i:i+2] == '->': if token: tokens.append(token) token = '' i += 2 continue elif c == '.': if token: tokens.append(token) token = '' i += 1 continue elif c == '[': # Заканчиваем текущий токен, если есть if token: tokens.append(token) token = '' # Собираем индекс [N] idx = '' while i < len(path) and path[i] != ']': idx += path[i] i += 1 if i < len(path) and path[i] == ']': idx += ']' i += 1 tokens.append(idx) continue else: token += c i += 1 if token: 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 = split_path(name) if len(name_parts) < len(search_parts): return False for sp, np in zip(search_parts, name_parts): if sp not in np: 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 class VariableSelectWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) self.expanded_vars = [] self.node_index = {} self.is_autocomplete_on = True # <--- ДОБАВИТЬ ЭТУ СТРОКУ self._bckspc_pressed = False self.manual_completion_active = False # --- UI Элементы --- self.search_input = QLineEdit() self.search_input.setPlaceholderText("Поиск...") self.tree = QTreeWidget() self.tree.setHeaderLabels(["Имя переменной", "Тип"]) self.tree.setSelectionMode(QTreeWidget.ExtendedSelection) self.tree.setRootIsDecorated(True) self.tree.setUniformRowHeights(True) self.tree.setStyleSheet(""" QTreeWidget::item:selected { background-color: #87CEFA; color: black; } QTreeWidget::item:hover { background-color: #D3D3D3; } """) self.completer = QCompleter() self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setFilterMode(Qt.MatchContains) self.completer.setWidget(self.search_input) # --- Layout --- layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.search_input) layout.addWidget(self.tree) # --- Соединения --- self.search_input.textChanged.connect(self.on_search_text_changed) self.search_input.installEventFilter(self) self.completer.activated[str].connect(self.insert_completion) # --- Публичные методы для управления виджетом снаружи --- def set_autocomplete(self, enabled: bool): """Включает или выключает режим автодополнения.""" self.is_autocomplete_on = enabled def set_data(self, vars_list): """Основной метод для загрузки данных в виджет.""" self.expanded_vars = vars_list # self.build_completion_list() # Если нужна полная перестройка списка self.populate_tree() def get_selected_items(self): """Возвращает выделенные элементы QTreeWidget.""" return self.tree.selectedItems() def get_selected_var_names(self): """Возвращает имена выделенных переменных.""" return [item.text(0) for item in self.tree.selectedItems() if item.text(0)] 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 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 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) def get_full_item_name(self, item): names = [] while item: names.append(item.text(0)) item = item.parent() return '.'.join(reversed(names)) def add_tree_item_recursively(self, parent, var): """ Рекурсивно добавляет переменную и её дочерние поля в дерево. Если parent == None, добавляет на верхний уровень. """ name = var['name'] type_str = var.get('type', '') show_var = var.get('show_var', 'false') == 'true' item = QTreeWidgetItem([name, type_str]) item.setData(0, Qt.UserRole, name) full_name = self.get_full_item_name(item) self.node_index[full_name.lower()] = item # Делаем bitfield-поля неактивными if "(bitfield:" in type_str: item.setDisabled(True) self.set_tool(item, "Битовые поля недоступны для выбора") for i, attr in enumerate(['file', 'extern', 'static']): item.setData(0, Qt.UserRole + 1 + i, var.get(attr)) if show_var: item.setForeground(0, Qt.gray) item.setForeground(1, Qt.gray) self.set_tool(item, "Уже добавлена") if parent is None: self.tree.addTopLevelItem(item) else: parent.addChild(item) for child in var.get('children', []): self.add_tree_item_recursively(item, child) def filter_tree(self): text = self.search_input.text().strip().lower() path_parts = split_path(text) if text else [] filtered_vars = filter_vars(self.expanded_vars, path_parts) # Сначала перерисовываем дерево self.populate_tree(filtered_vars) # Теперь 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) # Раскрываем путь до точного совпадения 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() def find_node_by_path(self, root_vars, path_list): current_level = root_vars node = None for part in path_list: node = None for var in current_level: if var['name'] == part: node = var break if node is None: 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 = split_path(text) path_parts = parts[:-1] if parts else [] prefix = parts[-1].lower() if parts else '' ends_with_sep = text.endswith('.') or text.endswith('->') or text.endswith('[') is_index_suggestion = text.endswith('[') completions = [] def find_exact_node(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: base_text = text[:-1] # убираем '[' parent_node = self.find_node_by_fullname(base_text) if not parent_node: 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()): child = parent_node.child(i) cname = child.text(0) m = re.match(rf'^{re.escape(base_text)}\[(\d+)\]$', cname) if m and cname not in seen: completions.append(cname) seen.add(cname) self.completer.setModel(QStringListModel(completions)) return completions if ends_with_sep: node = self.find_node_by_fullname(text[:-1]) if node: completions.extend(node.child(i).text(0) for i in range(node.childCount())) elif not path_parts: # Первый уровень — только если имя начинается с prefix for i in range(self.tree.topLevelItemCount()): item = self.tree.topLevelItem(i) name = item.text(0) if name.lower().startswith(prefix): completions.append(name) else: node = find_exact_node(path_parts) if node: for i in range(node.childCount()): child = node.child(i) name = child.text(0) name_parts = child.data(0, Qt.UserRole + 10) if name_parts is None: name_parts = 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): # ← строго startswith 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()) 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('->') or text.endswith('[')): # Определяем разделитель по имени первого ребёнка child_name = node.child(0).text(0) if child_name.startswith(text + '->'): text += '->' elif child_name.startswith(text + '.'): text += '.' elif '[' in child_name: text += '[' # для массивов else: text += '.' # fallback if not self._bckspc_pressed: self.search_input.setText(text) self.search_input.setCursorPosition(len(text)) 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: self.manual_completion_active = True text = self.search_input.text().strip() self.run_completions(text) elif event.key() == Qt.Key_Escape: # Esc — выключаем ручной режим и скрываем подсказки, если autocomplete выключен if not self.is_autocomplete_on: self.manual_completion_active = False self.completer.popup().hide() return True if event.key() == Qt.Key_Backspace: self._bckspc_pressed = True else: self._bckspc_pressed = False return super().eventFilter(obj, event) def run_completions(self, text): completions = self.update_completions(text) if not self.is_autocomplete_on and self._bckspc_pressed: text = text[:-1] 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(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.is_autocomplete_on: self.run_completions(text) else: # Если выключено, показываем подсказки только если флаг ручного вызова True if self.manual_completion_active: self.run_completions(text) else: self.completer.popup().hide() def build_completion_list(self): completions = [] def recurse(var, prefix=''): fullname = f"{prefix}.{var['name']}" if prefix else var['name'] completions.append(fullname) for child in var.get('children', []): recurse(child, fullname) for v in self.expanded_vars: recurse(v) self.all_completions = completions def set_tool(self, item, text): item.setToolTip(0, text) item.setToolTip(1, text)