оптимизировано. на первом запуске Выбора переменных конечно подвисает, но поиск работает относительно шустро

This commit is contained in:
Razvalyaev 2025-07-11 10:48:52 +03:00
parent ad7b9126b7
commit d3f1e824fa
3 changed files with 190 additions and 172 deletions

View File

@ -16,6 +16,7 @@ from VariableTable import VariableTableWidget, rows
from scanVarGUI import ProcessOutputWindowDummy from scanVarGUI import ProcessOutputWindowDummy
import scanVars import scanVars
import myXML import myXML
import time
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QApplication, QWidget, QTableWidget, QTableWidgetItem, QApplication, QWidget, QTableWidget, QTableWidgetItem,

View File

@ -7,6 +7,7 @@ from PySide2.QtGui import QKeySequence, QKeyEvent
from PySide2.QtCore import Qt, QStringListModel, QSettings from PySide2.QtCore import Qt, QStringListModel, QSettings
import setupVars import setupVars
import myXML import myXML
import time
array_re = re.compile(r'^(\w+)\[(\d+)\]$') array_re = re.compile(r'^(\w+)\[(\d+)\]$')
@ -18,6 +19,7 @@ class VariableSelectorDialog(QDialog):
self.setAttribute(Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_DeleteOnClose)
self.resize(600, 500) self.resize(600, 500)
self.selected_names = [] self.selected_names = []
self._bckspc_pressed = False # флаг подавления добавления разделителя
self.all_vars = all_vars self.all_vars = all_vars
self.structs = structs self.structs = structs
@ -153,88 +155,56 @@ class VariableSelectorDialog(QDialog):
self.add_tree_item_recursively(item, child) 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.tree.clear()
self.node_index.clear() self.node_index.clear()
for var in vars_list:
for var in self.expanded_vars:
self.add_tree_item_recursively(None, var) self.add_tree_item_recursively(None, var)
header = self.tree.header() header = self.tree.header()
header.setSectionResizeMode(QHeaderView.Interactive) # вручную можно менять header.setSectionResizeMode(QHeaderView.Interactive) # вручную можно менять
self.tree.setColumnWidth(0, 400) self.tree.setColumnWidth(0, 400)
self.tree.resizeColumnToContents(1) 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): def filter_tree(self):
text = self.search_input.text().strip().lower() 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) self.populate_tree(filtered_vars)
for i in range(item.childCount()):
hide_all(item.child(i))
def path_matches_search(name, search_parts): # Теперь node_index уже пересоздан — можно работать
name_parts = self.split_path(name.lower()) expand_level = len(path_parts) - 1 if path_parts else 0
if len(name_parts) < len(search_parts): for i in range(self.tree.topLevelItemCount()):
return False item = self.tree.topLevelItem(i)
for sp, np in zip(search_parts, name_parts): self.expand_to_level(item, expand_level)
if not np.startswith(sp):
return False
return True
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): def find_node_by_path(self, root_vars, path_list):
current_level = root_vars current_level = root_vars
@ -249,13 +219,13 @@ class VariableSelectorDialog(QDialog):
return None return None
current_level = node.get('children', []) current_level = node.get('children', [])
return node return node
def update_completions(self, text=None): def update_completions(self, text=None):
if text is None: if text is None:
text = self.search_input.text().strip() text = self.search_input.text().strip()
else: else:
text = text.strip() text = text.strip()
parts = self.split_path(text) parts = self.split_path(text)
path_parts = parts[:-1] if parts else [] path_parts = parts[:-1] if parts else []
prefix = parts[-1].lower() if parts else '' prefix = parts[-1].lower() if parts else ''
@ -264,29 +234,23 @@ class VariableSelectorDialog(QDialog):
completions = [] completions = []
def find_path_items(parts): def find_exact_node(parts):
items = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())] # Ищем точный узел по полному пути, используя node_index
for part in parts: # Постепенно собираем fullname из parts
part_lower = part.lower() if not parts:
matched = [] return None
for item in items: fullname = parts[0]
name_parts = self.split_path(item.text(0).lower()) for p in parts[1:]:
if name_parts and name_parts[-1] == part_lower: fullname += '.' + p
matched.append(item) return self.node_index.get(fullname.lower())
if not matched:
return []
items = []
for node in matched:
for i in range(node.childCount()):
items.append(node.child(i))
return matched
if is_index_suggestion: if is_index_suggestion:
# Предлагаем индексы (ищем всех детей с форматом foo[0], foo[1], ...) base_text = text[:-1] # убираем '['
base_text = text[:-1] # убираем '[', получаем, например, 'foo'
parent_node = self.find_node_by_fullname(base_text) parent_node = self.find_node_by_fullname(base_text)
if not parent_node: 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: if parent_node:
seen = set() seen = set()
for i in range(parent_node.childCount()): for i in range(parent_node.childCount()):
@ -300,11 +264,10 @@ class VariableSelectorDialog(QDialog):
return completions return completions
if ends_with_sep: if ends_with_sep:
# Завершён путь (например: project.adc[0].), показываем детей # Путь завершен, показываем детей узла
node = self.find_node_by_fullname(text[:-1]) node = self.find_node_by_fullname(text[:-1])
if node: if node:
for i in range(node.childCount()): completions.extend(node.child(i).text(0) for i in range(node.childCount()))
completions.append(node.child(i).text(0))
elif not path_parts: elif not path_parts:
# Первый уровень — по вхождению # Первый уровень — по вхождению
for i in range(self.tree.topLevelItemCount()): for i in range(self.tree.topLevelItemCount()):
@ -313,24 +276,28 @@ class VariableSelectorDialog(QDialog):
if prefix in name: if prefix in name:
completions.append(item.text(0)) completions.append(item.text(0))
else: else:
# Углубляемся: на последнем уровне используем startswith(prefix) node = find_exact_node(path_parts)
matched_items = find_path_items(path_parts) if node:
for item in matched_items: for i in range(node.childCount()):
for i in range(item.childCount()): child = node.child(i)
child = item.child(i)
name = child.text(0) 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: if not name_parts:
continue continue
last_part = name_parts[-1].lower() last_part = name_parts[-1].lower()
if prefix == '' or last_part.startswith(prefix): if prefix == '' or last_part.startswith(prefix):
completions.append(name) completions.append(name)
self.completer.setModel(QStringListModel(completions)) self.completer.complete()
return completions return completions
# Функция для поиска узла с полным именем # Функция для поиска узла с полным именем
def find_node_by_fullname(self, name): def find_node_by_fullname(self, name):
return self.node_index.get(name.lower()) return self.node_index.get(name.lower())
@ -348,86 +315,82 @@ class VariableSelectorDialog(QDialog):
text += '[' # для массивов text += '[' # для массивов
else: else:
text += '.' # fallback text += '.' # fallback
if not self._bckspc_pressed:
self.search_input.setText(text)
self.search_input.setCursorPosition(len(text))
self.search_input.setText(text) self.run_completions(text)
self.search_input.setCursorPosition(len(text))
self.update_completions()
self.completer.complete()
else: else:
self.search_input.setText(text) self.search_input.setText(text)
self.search_input.setCursorPosition(len(text)) self.search_input.setCursorPosition(len(text))
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
if obj == self.search_input and isinstance(event, QKeyEvent): if obj == self.search_input and isinstance(event, QKeyEvent):
if event.key() == Qt.Key_Space and event.modifiers() & Qt.ControlModifier: if event.key() == Qt.Key_Space and event.modifiers() & Qt.ControlModifier:
completions = self.update_completions()
self.completer.complete()
text = self.search_input.text().strip() text = self.search_input.text().strip()
self.run_completions(text)
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 True
if event.key() == Qt.Key_Backspace:
self._bckspc_pressed = True
else:
self._bckspc_pressed = False
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
def on_search_text_changed(self, text): def run_completions(self, text):
if self.autocomplete_checkbox.isChecked(): completions = self.update_completions(text)
completions = self.update_completions(text)
node = self.find_node_by_fullname(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: node = find_exact_item(completions[0])
if len(completions) > 1: if node and node.childCount() > 0:
should_show = True # Используем первую подсказку, чтобы определить нужный разделитель
elif len(completions) == 1: completions = self.update_completions(text + '.')
single_node = self.find_node_by_fullname(completions[0]) suggestion = 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: # Ищем, какой символ идёт после текущего текста
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.setModel(QStringListModel(completions))
self.completer.complete() 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() 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): def on_add_clicked(self):
self.selected_names = [] self.selected_names = []
@ -630,3 +593,36 @@ class VariableSelectorDialog(QDialog):
tokens.append(token) tokens.append(token)
return tokens 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

View File

@ -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: if "(" in field_type_str and "*" in field_type_str and ")" in field_type_str:
continue continue
# Обычное поле (не массив, не функция) # Проверим, является ли тип структурой (по имени)
child = { clean_type = scanVars.strip_ptr_and_array(field_type_str)
'name': full_name, struct_fields = structs.get(clean_type)
'type': field_type_str,
'pt_type': '', if isinstance(struct_fields, dict):
'iq_type': '', # Это одиночная структура — раскрываем рекурсивно
'return_type': '', sub_items = expand_struct_recursively(full_name, field_type_str, structs, typedefs, var_attrs, depth + 1)
'file': var_attrs.get('file'), child = {
'extern': var_attrs.get('extern'), 'name': full_name,
'static': var_attrs.get('static'), 'type': field_type_str,
} 'pt_type': '',
children.append(child) '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 continue
# Если поле — dict без 'type' или со сложной структурой, обрабатываем как вложенную структуру # Если поле — dict без 'type' или со сложной структурой, обрабатываем как вложенную структуру