import re from PySide6.QtWidgets import ( QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton, QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout ) from PySide6.QtGui import QKeySequence, QKeyEvent from PySide6.QtCore import Qt, QStringListModel, QSettings from setupVars import * from scanVars import * array_re = re.compile(r'^(\w+)\[(\d+)\]$') class VariableSelectorDialog(QDialog): def __init__(self, all_vars, structs, typedefs, xml_path=None, parent=None): super().__init__(parent) self.setWindowTitle("Выбор переменных") self.resize(600, 500) self.selected_names = [] self.all_vars = all_vars self.structs = structs self.typedefs = typedefs self.expanded_vars = [] self.var_map = {v['name']: v for v in all_vars} 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.on_search_text_changed) 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.btn_add = QPushButton("Добавить выбранные") self.btn_add.clicked.connect(self.on_add_clicked) 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.addLayout(search_layout) # заменили label и чекбокс layout.addWidget(self.search_input) layout.addWidget(self.tree) layout.addWidget(self.btn_add) layout.addWidget(self.btn_delete) self.setLayout(layout) self.populate_tree() 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) # Делаем 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 populate_tree(self): self.tree.clear() expanded_vars = expand_vars(self.all_vars, self.structs, self.typedefs) for var in expanded_vars: 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 filter_tree(self): text = self.search_input.text().strip().lower() path_parts = self.split_path(text) if text else [] def hide_all(item): item.setHidden(True) for i in range(item.childCount()): hide_all(item.child(i)) 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 def show_matching_path(item, level=0): name = item.text(0).lower() # Проверяем соответствие до длины path_parts if not path_parts: matched = True else: matched = False # Проверяем совпадение по пути if path_matches_search(name, path_parts[:level+1]): matched = True 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 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 = [] for item in self.tree.selectedItems(): name = item.text(0) # имя переменной (в колонке 1) type_str = item.text(1) # тип переменной (в колонке 2) if not name: continue self.selected_names.append((name, type_str)) if name in self.var_map: # Если переменная уже есть, просто включаем её и показываем var = self.var_map[name] var['show_var'] = 'true' var['enable'] = 'true' else: # Создаём новый элемент переменной # Получаем родительские параметры file_val = item.data(0, Qt.UserRole + 1) extern_val = item.data(0, Qt.UserRole + 2) static_val = item.data(0, Qt.UserRole + 3) new_var = { 'name': name, 'type': type_str, 'show_var': 'true', 'enable': 'true', 'shortname': name, 'pt_type': '', 'iq_type': '', 'return_type': 'iq_none', 'file': file_val, 'extern': str(extern_val).lower() if extern_val else 'false', 'static': str(static_val).lower() if static_val else 'false', } # Добавляем в список переменных self.all_vars.append(new_var) self.var_map[name] = new_var # Чтобы в будущем не добавлялось повторно self.accept() def on_delete_clicked(self): 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: 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() def set_tool(self, item, text): item.setToolTip(0, text) item.setToolTip(1, text) def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: self.delete_selected_vars() else: super().keyPressEvent(event) def delete_selected_vars(self): 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 не задан, невозможно удалить переменные.") 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 removed_any = False 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 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() 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)