import re import xml.etree.ElementTree as ET from PySide2.QtWidgets import ( QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton, QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout ) from PySide2.QtGui import QKeySequence, QKeyEvent from PySide2.QtCore import Qt, QStringListModel, QSettings import VariableTable import setupVars import myXML import time array_re = re.compile(r'^(\w+)\[(\d+)\]$') class VariableSelectorDialog(QDialog): def __init__(self, table, all_vars, structs, typedefs, xml_path=None, parent=None): super().__init__(parent) self.setWindowTitle("Выбор переменных") self.setAttribute(Qt.WA_DeleteOnClose) self.resize(600, 500) self.selected_names = [] self._bckspc_pressed = False # флаг подавления добавления разделителя self.table = table 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.node_index = {} self.xml_path = xml_path # сохраняем путь к xml self.manual_completion_active = False # --- Добавляем чекбокс для автодополнения --- 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.expanded_vars = setupVars.expand_vars(self.all_vars, self.structs, self.typedefs) self.build_completion_list() self.populate_tree() def get_full_item_name(self, item): names = [] while item: names.append(item.text(0)) item = item.parent() return '.'.join(reversed(names)) 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 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 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 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 = text.split('.') 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 = self.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): # Ищем точный узел по полному пути, используя 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: base_text = text[:-1] # убираем '[' parent_node = self.find_node_by_fullname(base_text) if not parent_node: # если 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()): 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: # Первый уровень — по вхождению for i in range(self.tree.topLevelItemCount()): item = self.tree.topLevelItem(i) name = item.text(0).lower() if prefix in name: completions.append(item.text(0)) else: node = find_exact_node(path_parts) if node: for i in range(node.childCount()): child = node.child(i) name = child.text(0) # Оптимизируем 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 prefix in last_part: # здесь изменено completions.append(name) 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.autocomplete_checkbox.isChecked(): 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.autocomplete_checkbox.isChecked() 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.autocomplete_checkbox.isChecked(): self.run_completions(text) else: # Если выключено, показываем подсказки только если флаг ручного вызова True if self.manual_completion_active: self.run_completions(text) else: self.completer.popup().hide() def on_add_clicked(self): self.selected_names = [] self.tree.setFocus() 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.done(QDialog.Accepted) def on_delete_clicked(self): selected_names = self._get_selected_var_names() if not selected_names: print("nothing selected") 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 PySide2.QtWidgets import QMessageBox QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.") return root, tree = myXML.safe_parse_xml(self.xml_path) 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') myXML.fwrite(root, self.xml_path) self.done(QDialog.Accepted) 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: print("nothing selected") 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 PySide2.QtWidgets import QMessageBox QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.") return root, tree = myXML.safe_parse_xml(self.xml_path) 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') myXML.fwrite(root, self.xml_path) self.table.populate(self.all_vars, self.structs, None) # Проверка пути к XML if not hasattr(self, 'xml_path') or not self.xml_path: from PySide2.QtWidgets import QMessageBox QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно удалить переменные.") return import xml.etree.ElementTree as ET root, tree = myXML.safe_parse_xml(self.xml_path) 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] # Удаляем из expanded_vars (тоже глобально) def filter_out_selected(vars_list): filtered = [] for v in vars_list: if v['name'] not in selected_names: # Рекурсивно фильтруем детей, если есть if 'children' in v: v = v.copy() v['children'] = filter_out_selected(v['children']) filtered.append(v) return filtered self.expanded_vars[:] = filter_out_selected(self.expanded_vars) if removed_any: myXML.fwrite(root, self.xml_path) self.filter_tree() def _get_selected_var_names(self): self.tree.setFocus() 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): """ Разбивает путь на компоненты: - '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 = 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