import re from PySide2.QtWidgets import ( QWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QLineEdit, QHeaderView, QCompleter ) from PySide2.QtGui import QKeyEvent from PySide2.QtCore import Qt, QStringListModel import pickle import time import hashlib def compute_vars_hash(vars_list): return hashlib.sha1(pickle.dumps(vars_list)).hexdigest() # Вспомогательные функции, которые теперь будут использоваться виджетом def split_path(path): """ Разбивает путь на компоненты: - 'foo[2].bar[1]->baz' → ['foo', '[2]', 'bar', '[1]', 'baz'] Если видит '-' в конце строки (без '>' после) — обрезает этот '-' """ tokens = [] token = '' i = 0 length = len(path) while i < length: c = path[i] # Разделители: '->' и '.' if c == '-' and i + 1 < length and path[i:i+2] == '->': if token: tokens.append(token) token = '' i += 2 continue elif c == '-' and i == length - 1: # '-' на конце строки без '>' после — просто пропускаем его i += 1 continue elif c == '.': if token: tokens.append(token) token = '' i += 1 continue elif c == '[': if token: tokens.append(token) token = '' idx = '' while i < length and path[i] != ']': idx += path[i] i += 1 if i < length and path[i] == ']': idx += ']' i += 1 tokens.append(idx) continue else: token += c i += 1 if token: tokens.append(token) return tokens # Функция парсит имя с индексами в (базовое_имя, список_индексов) def parse_name_with_indices(name): # Регулярка для имени и индексов: # имя - подряд букв/цифр/подчёркиваний, # индексы в квадратных скобках base_match = re.match(r'^([a-zA-Z_]\w*)', name) if not base_match: return name, [] # если не совпало, возвращаем как есть base_name = base_match.group(1) indices = re.findall(r'\[(\d+)\]', name) indices = list(map(int, indices)) return base_name, indices def match_path_part(search_part, node_part): # Если часть — индекс (вида [N]), требуем точное совпадение if search_part.startswith('[') and search_part.endswith(']'): return search_part == node_part # Если search_part содержит '[', значит поиск неполный индекс if '[' in search_part: return node_part.startswith(search_part) # Иначе — обычное имя: допускаем совпадение по префиксу return node_part.startswith(search_part) def show_matching_path(item, path_parts, level=0): node_name = item.text(0).lower() node_parts = split_path(node_name) if 'project' in node_name: a = 1 if level >= len(path_parts): # Путь полностью пройден — показываем только этот узел (без раскрытия всех детей) item.setHidden(False) item.setExpanded(False) return True if level >= len(node_parts): # Уровень поиска больше длины пути узла — скрываем item.setHidden(False) search_part = path_parts[level] node_part = node_parts[level] if search_part == node_part: # Точное совпадение — показываем узел, идём вглубь только по совпадениям item.setHidden(False) matched_any = False for i in range(item.childCount()): child = item.child(i) if show_matching_path(child, path_parts, level + 1): matched_any = True item.setExpanded(matched_any) return matched_any or item.childCount() == 0 elif node_part.startswith(search_part): # Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем item.setHidden(False) item.setExpanded(False) return True elif search_part in node_part and (level == len(path_parts)-1): # Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем item.setHidden(False) item.setExpanded(False) return True else: # Несовпадение — скрываем item.setHidden(True) return False 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 self._vars_hash = None # --- 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.tree.itemExpanded.connect(self.on_item_expanded) 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 = pickle.loads(pickle.dumps(vars_list, protocol=pickle.HIGHEST_PROTOCOL)) # self.build_completion_list() # Если нужна полная перестройка списка self.populate_tree() def populate_tree(self, vars_list=None): if vars_list is None: vars_list = self.expanded_vars new_hash = compute_vars_hash(vars_list) if self._vars_hash == new_hash: return self._vars_hash = new_hash self.tree.setUpdatesEnabled(False) self.tree.blockSignals(True) self.tree.clear() self.node_index.clear() for var in vars_list: self.add_tree_item_lazy(None, var) self.tree.setUpdatesEnabled(True) self.tree.blockSignals(False) header = self.tree.header() header.setSectionResizeMode(QHeaderView.Interactive) header.setSectionResizeMode(1, QHeaderView.Stretch) self.tree.setColumnWidth(0, 400) def on_item_expanded(self, item): if self.is_lazy_item(item): item.removeChild(item.child(0)) var = item.data(0, Qt.UserRole + 100) if var: for child_var in var.get('children', []): self.add_tree_item_lazy(item, child_var) def get_full_item_name(self, item): fullname = item.text(0) # Заменяем '->' на '.' fullname = fullname.replace('->', '.') fullname = fullname.replace('[', '.[') return fullname def add_tree_item_lazy(self, parent, var): name = var['name'] type_str = var.get('type', '') 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 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 parent is None: self.tree.addTopLevelItem(item) else: parent.addChild(item) # Если есть дети — добавляем заглушку (чтобы можно было раскрыть) if var.get('children'): dummy = QTreeWidgetItem(["lazy_marker"]) item.addChild(dummy) # Кэшируем детей для подгрузки по событию item.setData(0, Qt.UserRole + 100, var) # Сохраняем var целиком def filter_tree(self): text = self.search_input.text().strip().lower() path_parts = split_path(text) if text else [] if '.' not in text and '->' 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) # Не сбрасываем expanded, чтобы можно было раскрывать вручную else: item.setHidden(True) else: for i in range(self.tree.topLevelItemCount()): item = self.tree.topLevelItem(i) show_matching_path(item, path_parts, 0) 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() normalized_text = text.replace('->', '.') 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) if child.isHidden(): continue 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: for i in range(node.childCount()): child = node.child(i) if child.isHidden(): continue completions.append(child.text(0)) elif not path_parts: # Первый уровень — только если имя начинается с prefix for i in range(self.tree.topLevelItemCount()): item = self.tree.topLevelItem(i) if item.isHidden(): continue 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) if child.isHidden(): continue 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 prefix in last_part: # ← строго startswith completions.append(name) self.completer.setModel(QStringListModel(completions)) self.completer.complete() return completions # Функция для поиска узла с полным именем def find_node_by_fullname(self, name): if name is None: return None normalized_name = name.replace('->', '.').lower() normalized_name = normalized_name.replace('[', '.[').lower() return self.node_index.get(normalized_name) 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 + '.') if not completions: return 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) def get_all_items(self): """Возвращает все конечные (leaf) элементы, исключая битовые поля и элементы с детьми (реальными).""" def collect_leaf_items(parent): leaf_items = [] for i in range(parent.childCount()): child = parent.child(i) if child.isHidden(): continue # Если есть заглушка — раскрываем self.on_item_expanded(child) if child.childCount() == 0: item_type = child.text(1) if item_type and 'bitfield' in str(item_type).lower(): continue leaf_items.append(child) else: leaf_items.extend(collect_leaf_items(child)) return leaf_items all_leaf_items = [] for i in range(self.tree.topLevelItemCount()): top = self.tree.topLevelItem(i) # Раскрываем lazy, если надо self.on_item_expanded(top) if top.childCount() == 0: item_type = top.text(1) if item_type and 'bitfield' in str(item_type).lower(): continue all_leaf_items.append(top) else: all_leaf_items.extend(collect_leaf_items(top)) return all_leaf_items def _get_internal_selected_items(self): """Возвращает выделенные элементы и всех их потомков, включая lazy.""" selected = self.tree.selectedItems() all_items = [] def collect_children(item): # Раскрываем при необходимости # Раскрываем lazy, если надо self.on_item_expanded(item) items = [item] for i in range(item.childCount()): child = item.child(i) items.extend(collect_children(child)) return items for item in selected: all_items.extend(collect_children(item)) return all_items def get_selected_items(self): """Возвращает только конечные (leaf) выделенные элементы, исключая bitfield.""" selected = self.tree.selectedItems() leaf_items = [] for item in selected: # Раскрываем lazy, если надо self.on_item_expanded(item) # Если у узла нет видимых/выделенных детей — он лист if all(item.child(i).isHidden() or not item.child(i).isSelected() for i in range(item.childCount())): item_type = item.data(0, Qt.UserRole) if item_type and 'bitfield' in str(item_type).lower(): continue leaf_items.append(item) return leaf_items def is_lazy_item(self, item): return item.childCount() == 1 and item.child(0).text(0) == 'lazy_marker' def get_all_var_names(self): """Возвращает имена всех конечных (leaf) переменных, исключая битовые поля и группы.""" return [item.text(0) for item in self.get_all_items() if item.text(0)] def _get_internal_selected_var_names(self): """Возвращает имена выделенных переменных.""" return [item.text(0) for item in self._get_internal_selected_items() if item.text(0)] def get_selected_var_names(self): """Возвращает имена только конечных (leaf) переменных из выделенных.""" return [item.text(0) for item in self.get_selected_items() if item.text(0)]