debugVarTool/Src/selectTable.py
Razvalyaev 42ac3eb65d наконец-то вроде сделан поиск.
есть кривости в подскасках
начата работа надо двумя таблицами (всех и выбранных переменных)
2025-07-13 18:53:01 +03:00

525 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
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
# --- 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 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()
fullname = '.'.join(reversed(names))
# Заменяем '->' на '.'
fullname = fullname.replace('->', '.')
return fullname
def get_selected_vars(self):
selected = self.tree.selectedItems()
result = []
for item in selected:
var = item.data(0, Qt.UserRole)
if var:
result.append(var)
return result
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 []
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 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):
if name is None:
return None
normalized_name = 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):
return [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())]
def get_all_var_names(self):
return [self.tree.topLevelItem(i).text(0) for i in range(self.tree.topLevelItemCount())]