добавлен readme
другая библиотека для xml (более быстрая) сделан выбор элементов в выпадающем списке типов исправлено вроде кривое выставлениеп return_type изменено окно выбора переменых - кнопки добавит, удалить заменены на применить - исправлены кривые подсказки в .exe версии
This commit is contained in:
parent
7b720cbdf4
commit
cb496bca0f
BIN
DebugVarEdit.exe
BIN
DebugVarEdit.exe
Binary file not shown.
136
README.md
Normal file
136
README.md
Normal file
@ -0,0 +1,136 @@
|
||||
# DebugVarEdit — Утилита для генерации отладочных переменных C-проекта
|
||||
|
||||
**DebugVarEdit** — графическое приложение для Windows, предназначенное для настройки и генерации отладочных переменных (`debug_vars.c`) на основе исходного C-проекта. Работает с `makefile` проекта, сохраняет изменения в XML и позволяет удобно редактировать переменные и их типы через интерфейс.
|
||||
|
||||
Программа — один исполняемый файл `DebugVarEdit.exe`, не требующий установки и дополнительных зависимостей.
|
||||
|
||||
> Требуется Windows 7 или новее.
|
||||
---
|
||||
|
||||
## Как использовать
|
||||
|
||||
1. Запустите **DebugVarEdit.exe.**
|
||||
|
||||
2. Укажите пути:
|
||||
- **Путь к XML** — новый или существующий файл настроек переменных;
|
||||
- **Путь к проекту** — путь к корню проека (папка, которая является корнем для проекта в Code Composer);
|
||||
- **Путь к makefile** — путь к `makefile` относительно корня проекта;
|
||||
- **Путь для debug_vars.c** — папка, куда будет сгенерирован файл `debug_vars.c` с переменными.
|
||||
|
||||
3. Нажмите **Сканировать переменные**:
|
||||
- Программа проанализирует исходники, указанные в makefile, и найдёт все переменные.
|
||||
- Результат сохранится в двух XML-файлах:
|
||||
- `structs.xml` — информация обо всех структурах и typedef;
|
||||
- `<ваш_файл>.xml` — список всех найденных переменных.
|
||||
|
||||
4. Нажмите **Добавить переменные**:
|
||||
- В **левой таблице** отображаются все найденные переменные.
|
||||
Для добавления переменной в проект дважды кликните по ней или нажмите кнопку `>`, чтобы переместить в правую таблицу.
|
||||
- В **правой таблице** находятся переменные, выбранные для использования.
|
||||
Чтобы убрать переменную из проекта, переместите её обратно в левую таблицу двойным кликом или кнопкой `<`.
|
||||
- После выбора переменных нажмите **Применить**, чтобы обновить основной список переменных и включить их в проект.
|
||||
|
||||
5. Настройте параметры выбранных переменных:
|
||||
- **En** — включение или отключение переменной для генерации;
|
||||
- **Base Type** — базовый тип переменной (например, int8, uint16 и т.д.);
|
||||
- **IQ Type** — формат IQ, если применимо;
|
||||
- **Return Type** — формат возвращаемого значения (IQ-тип или целочисленный);
|
||||
- **Shortname** — короткое имя переменной для для терминалки.
|
||||
|
||||
Все параметры выбираются из выпадающих списков, которые можно настроить, чтобы отображались только нужные опции.
|
||||
|
||||
6. Нажмите **Сгенерировать файл** для создания файла `debug_vars.c` с выбранными переменными.
|
||||
---
|
||||
|
||||
## Возможности
|
||||
|
||||
- Загрузка и сохранение настроек переменных в XML-файлах.
|
||||
- Автоматическое определение исходных файлов с переменными для удобства работы.
|
||||
- Редактирование переменных: включение, короткого имени и типов через удобные списки.
|
||||
- Подсветка ошибок при вводе (неправильные имена, слишком длинные короткие имена).
|
||||
- Быстрая фильтрация переменных по столбцам.
|
||||
- Автоматическая генерация файла debug_vars.c с выбранными переменными.
|
||||
- Возможность сразу открыть сгенерированный файл в редакторе.
|
||||
- Умное автодополнение имён переменных и полей структур.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Пример XML-файла
|
||||
|
||||
```xml
|
||||
<project proj_path="C:/myproj" makefile_path="Debug/Makefile" structs_path="debugVars/structs.xml">
|
||||
<variables>
|
||||
<var name="g_myvar">
|
||||
<enable>true</enable>
|
||||
<show_var>true</show_var>
|
||||
<shortname>myv</shortname>
|
||||
<pt_type>pt_float</pt_type>
|
||||
<iq_type>t_iq24</iq_type>
|
||||
<return_type>t_iq24</return_type>
|
||||
<type>float</type>
|
||||
<file>Src/main/main.c</file>
|
||||
<extern>true</extern>
|
||||
<static>false</static>
|
||||
</var>
|
||||
</variables>
|
||||
</project>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Для разработчиков
|
||||
|
||||
### Структура проекта:
|
||||
|
||||
```bash
|
||||
Src
|
||||
├── DebugVarEdit_GUI.py # Главное окно
|
||||
├── VariableTable.py # Таблица выбранных переменных
|
||||
├── VariableSelector.py # Окно выбора переменных
|
||||
├── selectTable.py # Таблица переменных в окне выбора переменных
|
||||
├── scanVarGUI.py # Отображение процесса сканирования переменных
|
||||
├── scanVar.py # Сканирование переменных среди .c/.h файлов
|
||||
├── generateVars.py # Генерация debug_vars.c
|
||||
├── myXML.py # Утилиты для XML
|
||||
├── parseMakefile.py # Парсинг makefile на .c/.h файлы
|
||||
├── setupVars.py # Подготовка переменных для окна выбора переменных
|
||||
├── libclang.dll # Бибилиотека clang
|
||||
├── icon.ico # Иконка
|
||||
build
|
||||
├── build_and_clean.py # Билдинг проекта в .exe (через nuitka или pyinstaller)
|
||||
```
|
||||
|
||||
### Зависимости
|
||||
|
||||
Для запуска приложения:
|
||||
- **Python 3.7+**
|
||||
- **clang** — используется для парсинга C-кода (требуется `libclang.dll`)
|
||||
- **PySide2** — GUI-фреймворк
|
||||
- **lxml** — работа с XML
|
||||
> Python 3.7 и PySide2 рекомендуется для совместимости с Windows 7
|
||||
|
||||
Для сборки `.exe`:
|
||||
- **Nuitka** — создает полностью автономный `.exe` без внешних зависимостей
|
||||
- **PyInstaller** — создает `.exe` с зависимостью от `pythonXX.dll` (Python должен быть установлен)
|
||||
|
||||
|
||||
### Сборка:
|
||||
Если вы хотите собрать `DebugVarEdit.exe` самостоятельно из исходников, используйте скрипт **build/build_and_clean.py**. Он автоматически собирает проект с помощью Nuitka или PyInstaller:
|
||||
- Собирает проект в `DebugVarEdit.exe` в корневой папке.
|
||||
- Включает:
|
||||
- все необходимые `.dll` (например, `libclang.dll`),
|
||||
- иконку (`icon.ico`),
|
||||
- плагины PySide2.
|
||||
- Очищает временные папки после сборки:
|
||||
- `build_temp`
|
||||
- `__pycache__`
|
||||
- `DebugVarEdit_GUI.*`
|
||||
|
||||
> Все пути, имена файлов, временные папки и выбор между Nuitka и PyInstaller можно настроить в начале файла `build_and_clean.py`.
|
||||
|
||||
### Установка зависимостей
|
||||
|
||||
```bash
|
||||
pip install PySide2 lxml nuitka pyinstaller
|
||||
```
|
@ -4,7 +4,7 @@
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
import lxml.etree as ET
|
||||
from generateVars import type_map
|
||||
from enum import IntEnum
|
||||
import threading
|
||||
@ -31,12 +31,18 @@ var_edit_title = "Редактор переменных для отладки"
|
||||
xml_path_title = "Путь к XML:"
|
||||
proj_path_title = "Путь к проекту:"
|
||||
makefile_path_title = "Пусть к makefile (относительно проекта)"
|
||||
output_path_title = "Папка для debug_vars.c:"
|
||||
output_path_title = "Путь для для debug_vars.c:"
|
||||
scan_title = "Сканировать переменные"
|
||||
build_title = "Сгенерировать файл"
|
||||
add_vars_title = "Добавить переменные"
|
||||
open_output_title = "Открыть файл"
|
||||
|
||||
def set_sub_elem_text(parent, tag, text):
|
||||
el = parent.find(tag)
|
||||
if el is None:
|
||||
el = ET.SubElement(parent, tag)
|
||||
el.text = str(text)
|
||||
|
||||
# 3. UI: таблица с переменными
|
||||
class VarEditor(QWidget):
|
||||
def __init__(self):
|
||||
@ -242,8 +248,8 @@ class VarEditor(QWidget):
|
||||
QMessageBox.critical(self, "Ошибка при генерации", str(e))
|
||||
|
||||
|
||||
def update(self):
|
||||
if self._updating:
|
||||
def update(self, force=0):
|
||||
if self._updating and (force==0):
|
||||
return # Уже в процессе обновления — выходим, чтобы избежать рекурсии
|
||||
self._updating = True
|
||||
|
||||
@ -407,8 +413,7 @@ class VarEditor(QWidget):
|
||||
return
|
||||
|
||||
try:
|
||||
self.update_all_paths()
|
||||
self.update()
|
||||
self.update(1)
|
||||
|
||||
except Exception as e:
|
||||
self.makefile_path = None
|
||||
@ -443,10 +448,21 @@ class VarEditor(QWidget):
|
||||
self.write_to_xml()
|
||||
self.update()
|
||||
|
||||
|
||||
def write_to_xml(self, dummy=None):
|
||||
t0 = time.time()
|
||||
self.update_all_paths()
|
||||
|
||||
def get_val(name, default=''):
|
||||
return str(v_table[name] if v_table and name in v_table else v.get(name, default))
|
||||
|
||||
def element_differs(elem, values: dict):
|
||||
for tag, new_val in values.items():
|
||||
current_elem = elem.find(tag)
|
||||
current_val = (current_elem.text or '').strip()
|
||||
new_val_str = str(new_val or '').strip()
|
||||
if current_val != new_val_str:
|
||||
return True
|
||||
return False
|
||||
|
||||
if not self.xml_path or not os.path.isfile(self.xml_path):
|
||||
print("XML файл не найден или путь пустой")
|
||||
return
|
||||
@ -470,7 +486,6 @@ class VarEditor(QWidget):
|
||||
|
||||
if self.makefile_path and os.path.isfile(self.makefile_path):
|
||||
rel_makefile = myXML.make_relative_path(self.makefile_path, self.proj_path)
|
||||
root.set("makefile_path", rel_makefile)
|
||||
# Если результат — абсолютный путь, не записываем
|
||||
if not os.path.isabs(rel_makefile):
|
||||
root.set("makefile_path", rel_makefile)
|
||||
@ -481,6 +496,7 @@ class VarEditor(QWidget):
|
||||
if not os.path.isabs(rel_struct):
|
||||
root.set("structs_path", rel_struct)
|
||||
|
||||
t1 = time.time()
|
||||
|
||||
vars_elem = root.find('variables')
|
||||
if vars_elem is None:
|
||||
@ -495,6 +511,7 @@ class VarEditor(QWidget):
|
||||
'extern': var_elem.findtext('extern', ''),
|
||||
'static': var_elem.findtext('static', '')
|
||||
}
|
||||
var_elements_by_name = {ve.attrib.get('name'): ve for ve in vars_elem.findall('var')}
|
||||
|
||||
# Читаем переменные из таблицы (активные/изменённые)
|
||||
table_vars = {v['name']: v for v in self.table.read_data()}
|
||||
@ -503,63 +520,73 @@ class VarEditor(QWidget):
|
||||
|
||||
# Объединённый список переменных для записи
|
||||
all_names = list(all_vars_by_name.keys())
|
||||
t2 = time.time()
|
||||
for name in all_names:
|
||||
v = all_vars_by_name[name]
|
||||
v_table = table_vars.get(name)
|
||||
var_elem = None
|
||||
|
||||
# Ищем уже существующий <var> в XML
|
||||
for ve in vars_elem.findall('var'):
|
||||
if ve.attrib.get('name') == name:
|
||||
var_elem = ve
|
||||
break
|
||||
if var_elem is None:
|
||||
var_elem = ET.SubElement(vars_elem, 'var', {'name': name})
|
||||
|
||||
def set_sub_elem_text(parent, tag, text):
|
||||
el = parent.find(tag)
|
||||
if el is None:
|
||||
el = ET.SubElement(parent, tag)
|
||||
el.text = str(text)
|
||||
|
||||
pt_type_val = v_table['pt_type'] if v_table and 'pt_type' in v_table else v.get('pt_type', '')
|
||||
|
||||
pt_type_val = get_val('pt_type').lower()
|
||||
if 'arr' in pt_type_val or 'struct' in pt_type_val or 'union' in pt_type_val:
|
||||
continue
|
||||
|
||||
show_var_val = str(v.get('show_var', 'false')).lower()
|
||||
enable_val = str(v_table['enable'] if v_table and 'enable' in v_table else v.get('enable', 'false')).lower()
|
||||
|
||||
set_sub_elem_text(var_elem, 'show_var', show_var_val)
|
||||
set_sub_elem_text(var_elem, 'enable', enable_val)
|
||||
enable_val = get_val('enable').lower()
|
||||
|
||||
# Тут подтягиваем из таблицы, если есть, иначе из v
|
||||
shortname_val = v_table['shortname'] if v_table and 'shortname' in v_table else v.get('shortname', '')
|
||||
iq_type_val = v_table['iq_type'] if v_table and 'iq_type' in v_table else v.get('iq_type', '')
|
||||
ret_type_val = v_table['return_type'] if v_table and 'return_type' in v_table else v.get('return_type', '')
|
||||
shortname_val = get_val('shortname')
|
||||
iq_type_val = get_val('iq_type')
|
||||
ret_type_val = get_val('return_type')
|
||||
|
||||
set_sub_elem_text(var_elem, 'shortname', shortname_val)
|
||||
set_sub_elem_text(var_elem, 'pt_type', pt_type_val)
|
||||
set_sub_elem_text(var_elem, 'iq_type', iq_type_val)
|
||||
set_sub_elem_text(var_elem, 'return_type', ret_type_val)
|
||||
set_sub_elem_text(var_elem, 'type', v.get('type', ''))
|
||||
|
||||
# file/extern/static: из original_info, либо из v
|
||||
file_val = v.get('file') or original_info.get(name, {}).get('file', '')
|
||||
extern_val = v.get('extern') or original_info.get(name, {}).get('extern', '')
|
||||
static_val = v.get('static') or original_info.get(name, {}).get('static', '')
|
||||
|
||||
set_sub_elem_text(var_elem, 'file', file_val)
|
||||
set_sub_elem_text(var_elem, 'extern', extern_val)
|
||||
set_sub_elem_text(var_elem, 'static', static_val)
|
||||
values_to_write = {
|
||||
'show_var': show_var_val,
|
||||
'enable': enable_val,
|
||||
'shortname': shortname_val,
|
||||
'pt_type': pt_type_val,
|
||||
'iq_type': iq_type_val,
|
||||
'return_type': ret_type_val,
|
||||
'type': v.get('type', ''),
|
||||
'file': file_val,
|
||||
'extern': extern_val,
|
||||
'static': static_val
|
||||
}
|
||||
|
||||
# Ищем уже существующий <var> в XML
|
||||
var_elem = var_elements_by_name.get(name)
|
||||
# Если элемента нет, это новая переменная — сразу пишем
|
||||
if var_elem is None:
|
||||
var_elem = ET.SubElement(vars_elem, 'var', {'name': name})
|
||||
var_elements_by_name[name] = var_elem
|
||||
write_all = True # обязательно записать все поля
|
||||
else:
|
||||
write_all = element_differs(var_elem, values_to_write)
|
||||
|
||||
if not write_all:
|
||||
continue # Пропускаем, если нет изменений
|
||||
|
||||
for tag, text in values_to_write.items():
|
||||
set_sub_elem_text(var_elem, tag, text)
|
||||
|
||||
t3 = time.time()
|
||||
# Преобразуем дерево в строку
|
||||
self.table.check()
|
||||
myXML.fwrite(root, self.xml_path)
|
||||
|
||||
self.table.check()
|
||||
t4 = time.time()
|
||||
'''print(f"[T1] parse + set paths: {t1 - t0:.3f} сек")
|
||||
print(f"[T2] prepare variables: {t2 - t1:.3f} сек")
|
||||
print(f"[T3] loop + updates: {t3 - t2:.3f} сек")
|
||||
print(f"[T4] write to file: {t4 - t3:.3f} сек")
|
||||
print(f"[TOTAL] write_to_xml total: {t4 - t0:.3f} сек")'''
|
||||
except Exception as e:
|
||||
print(f"Ошибка при сохранении XML: {e}")
|
||||
|
||||
|
||||
def __open_output_file_with_program(self):
|
||||
output_path = self.get_output_path()
|
||||
if not output_path:
|
||||
|
@ -1,5 +1,5 @@
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
import lxml.etree as ET
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton,
|
||||
QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout, QSizePolicy
|
||||
@ -52,12 +52,15 @@ class VariableSelectorDialog(QDialog):
|
||||
self.btn_left.clicked.connect(self.on_move_left)
|
||||
|
||||
# Создаем кнопки, они остаются в диалоге
|
||||
self.btn_add = QPushButton("Добавить выбранные")
|
||||
self.btn_delete = QPushButton("Удалить выбранные")
|
||||
self.btn_accept = QPushButton("Применить")
|
||||
|
||||
# Создаем экземпляр вашего готового виджета
|
||||
self.vars_widget = selectTable.VariableSelectWidget(self)
|
||||
self.vars_widget.tree.itemDoubleClicked.connect(self.on_left_tree_double_click)
|
||||
self.vars_widget.setObjectName("LeftTable")
|
||||
self.selected_vars_widget = selectTable.VariableSelectWidget(self)
|
||||
self.selected_vars_widget.tree.itemDoubleClicked.connect(self.on_rigth_tree_double_click)
|
||||
self.selected_vars_widget.setObjectName("RightTable")
|
||||
|
||||
|
||||
# --- Лэйауты ---
|
||||
@ -86,8 +89,7 @@ class VariableSelectorDialog(QDialog):
|
||||
|
||||
# Кнопки "Добавить выбранные" и "Удалить выбранные" под таблицами
|
||||
buttons_layout = QVBoxLayout()
|
||||
buttons_layout.addWidget(self.btn_add)
|
||||
buttons_layout.addWidget(self.btn_delete)
|
||||
buttons_layout.addWidget(self.btn_accept)
|
||||
main_layout.addLayout(buttons_layout)
|
||||
|
||||
# Важно, если окно — QDialog или QWidget, установи layout
|
||||
@ -96,8 +98,7 @@ class VariableSelectorDialog(QDialog):
|
||||
|
||||
|
||||
# Соединяем сигналы кнопок с методами диалога
|
||||
self.btn_add.clicked.connect(self.on_add_clicked)
|
||||
self.btn_delete.clicked.connect(self.on_delete_clicked)
|
||||
self.btn_accept.clicked.connect(self.on_apply_clicked)
|
||||
|
||||
# Соединяем чекбокс с методом виджета
|
||||
self.autocomplete_checkbox.stateChanged.connect(self.vars_widget.set_autocomplete)
|
||||
@ -119,6 +120,7 @@ class VariableSelectorDialog(QDialog):
|
||||
for var in data:
|
||||
if var['name'] in selected:
|
||||
var['show_var'] = 'true'
|
||||
var['enable'] = 'true'
|
||||
if 'children' in var:
|
||||
mark_selected_show_var(var['children'])
|
||||
mark_selected_show_var(self.expanded_vars)
|
||||
@ -159,15 +161,15 @@ class VariableSelectorDialog(QDialog):
|
||||
t5 = time.perf_counter()
|
||||
self.selected_vars_widget.filter_tree()
|
||||
|
||||
|
||||
def on_add_clicked(self):
|
||||
# Получаем все переменные из правой таблицы (selected_vars_widget)
|
||||
var_names = self.selected_vars_widget.get_all_var_names()
|
||||
def on_apply_clicked(self):
|
||||
# Получаем имена всех переменных из правой таблицы (selected_vars_widget)
|
||||
right_var_names = set(self.selected_vars_widget.get_all_var_names())
|
||||
all_items = self.selected_vars_widget.get_all_items()
|
||||
if not all_items:
|
||||
return
|
||||
|
||||
def add_to_var_map_recursively(item):
|
||||
# Устанавливаем show_var=true и enable=true для переменных из правой таблицы
|
||||
def add_or_update_var(item):
|
||||
name = item.text(0)
|
||||
type_str = item.text(1)
|
||||
|
||||
@ -180,9 +182,15 @@ class VariableSelectorDialog(QDialog):
|
||||
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': 't_iq_none', 'file': file_val,
|
||||
'name': name,
|
||||
'type': type_str,
|
||||
'show_var': 'true',
|
||||
'enable': 'true',
|
||||
'shortname': name,
|
||||
'pt_type': '',
|
||||
'iq_type': '',
|
||||
'return_type': 't_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',
|
||||
}
|
||||
@ -190,71 +198,74 @@ class VariableSelectorDialog(QDialog):
|
||||
self.var_map[name] = new_var
|
||||
|
||||
for item in all_items:
|
||||
add_to_var_map_recursively(item)
|
||||
add_or_update_var(item)
|
||||
|
||||
self.accept()
|
||||
# Сбрасываем show_var и enable у всех переменных, которых нет в правой таблице
|
||||
for var in self.all_vars:
|
||||
if var['name'] not in right_var_names:
|
||||
var['show_var'] = 'false'
|
||||
var['enable'] = 'false'
|
||||
|
||||
# Обновляем expanded_vars чтобы отразить новые show_var и enable
|
||||
def update_expanded_vars(data):
|
||||
for v in data:
|
||||
name = v['name']
|
||||
if name in self.var_map:
|
||||
v['show_var'] = self.var_map[name]['show_var']
|
||||
v['enable'] = self.var_map[name]['enable']
|
||||
if 'children' in v:
|
||||
update_expanded_vars(v['children'])
|
||||
update_expanded_vars(self.expanded_vars)
|
||||
|
||||
def on_delete_clicked(self):
|
||||
# Получаем все элементы (QTreeWidgetItem) из правой таблицы
|
||||
all_items = self.selected_vars_widget.get_all_items()
|
||||
if not all_items:
|
||||
return
|
||||
|
||||
affected_names = []
|
||||
|
||||
def disable_var_recursively(item):
|
||||
name = item.text(0)
|
||||
if name in self.var_map:
|
||||
self.var_map[name]['show_var'] = 'false'
|
||||
self.var_map[name]['enable'] = 'false'
|
||||
affected_names.append(name)
|
||||
|
||||
# Рекурсивно отключаем детей
|
||||
for i in range(item.childCount()):
|
||||
child = item.child(i)
|
||||
disable_var_recursively(child)
|
||||
|
||||
for item in all_items:
|
||||
disable_var_recursively(item)
|
||||
|
||||
# Обновляем XML и таблицу
|
||||
self.update_xml_vars(affected_names, 'false', 'false')
|
||||
# Обновляем отображение в виджетах
|
||||
self.update_vars_widget()
|
||||
|
||||
# Закрываем диалог
|
||||
self.accept()
|
||||
|
||||
def update_xml_vars(self, names, show, enable):
|
||||
"""Обновляет флаги show_var и enable в XML файле, только если у переменной нет вложенных структур."""
|
||||
if not self.xml_path:
|
||||
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'):
|
||||
if var_elem.attrib.get('name') in names:
|
||||
# Проверяем наличие вложенных структур или объединений
|
||||
has_nested_structs = any(
|
||||
var_elem.find(tag) is not None for tag in ('struct', 'union')
|
||||
)
|
||||
if not has_nested_structs:
|
||||
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', show)
|
||||
set_text('enable', enable)
|
||||
|
||||
myXML.fwrite(root, self.xml_path)
|
||||
|
||||
|
||||
def save_checkbox_state(self):
|
||||
self.settings.setValue("autocomplete_enabled", self.autocomplete_checkbox.isChecked())
|
||||
|
||||
|
||||
|
||||
|
||||
# Обнови on_left_tree_double_click:
|
||||
def on_left_tree_double_click(self, item, column):
|
||||
selected_names = [item.text(0)]
|
||||
if not selected_names:
|
||||
return
|
||||
|
||||
def mark_selected_show_var(data):
|
||||
for var in data:
|
||||
if var['name'] in selected_names:
|
||||
var['show_var'] = 'true'
|
||||
var['enable'] = 'true'
|
||||
if 'children' in var:
|
||||
mark_selected_show_var(var['children'])
|
||||
mark_selected_show_var(self.expanded_vars)
|
||||
|
||||
self.update_vars_widget()
|
||||
|
||||
# Добавь обработчик двойного клика справа (если нужно):
|
||||
def on_rigth_tree_double_click(self, item, column):
|
||||
selected_names = [item.text(0)]
|
||||
if not selected_names:
|
||||
return
|
||||
|
||||
def mark_selected_hide_var(data):
|
||||
for var in data:
|
||||
if var['name'] in selected_names:
|
||||
var['show_var'] = 'false'
|
||||
if 'children' in var:
|
||||
mark_selected_hide_var(var['children'])
|
||||
mark_selected_hide_var(self.expanded_vars)
|
||||
|
||||
self.update_vars_widget()
|
||||
|
||||
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Delete:
|
||||
self.delete_selected_vars()
|
||||
@ -314,7 +325,6 @@ class VariableSelectorDialog(QDialog):
|
||||
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
|
||||
|
@ -1,11 +1,14 @@
|
||||
from PySide2.QtWidgets import (
|
||||
QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QCompleter,
|
||||
QAbstractItemView, QHeaderView, QLabel
|
||||
QAbstractItemView, QHeaderView, QLabel,
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget
|
||||
)
|
||||
from PySide2.QtGui import QColor, QBrush, QPalette
|
||||
from PySide2.QtCore import Qt
|
||||
from enum import IntEnum
|
||||
from generateVars import type_map
|
||||
import time
|
||||
from typing import Dict, List
|
||||
|
||||
class rows(IntEnum):
|
||||
No = 0
|
||||
@ -17,6 +20,46 @@ class rows(IntEnum):
|
||||
ret_type = 6
|
||||
short_name = 7
|
||||
|
||||
class FilterDialog(QDialog):
|
||||
def __init__(self, parent, options, selected, title="Выберите значения"):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(title)
|
||||
self.resize(250, 300)
|
||||
self.selected = set(selected)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
scroll = QScrollArea(self)
|
||||
scroll.setWidgetResizable(True)
|
||||
container = QWidget()
|
||||
scroll.setWidget(container)
|
||||
|
||||
self.checkboxes = []
|
||||
vbox = QVBoxLayout(container)
|
||||
|
||||
for opt in options:
|
||||
cb = QCheckBox(opt)
|
||||
cb.setChecked(opt in self.selected)
|
||||
vbox.addWidget(cb)
|
||||
self.checkboxes.append(cb)
|
||||
|
||||
layout.addWidget(scroll)
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_ok = QPushButton("OK")
|
||||
btn_cancel = QPushButton("Отмена")
|
||||
btn_layout.addWidget(btn_ok)
|
||||
btn_layout.addWidget(btn_cancel)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
btn_ok.clicked.connect(self.accept)
|
||||
btn_cancel.clicked.connect(self.reject)
|
||||
|
||||
def get_selected(self):
|
||||
return [cb.text() for cb in self.checkboxes if cb.isChecked()]
|
||||
|
||||
|
||||
|
||||
class VariableTableWidget(QTableWidget):
|
||||
def __init__(self, parent=None):
|
||||
@ -27,7 +70,7 @@ class VariableTableWidget(QTableWidget):
|
||||
'En',
|
||||
'Name',
|
||||
'Origin Type',
|
||||
'Pointer Type',
|
||||
'Base Type',
|
||||
'IQ Type',
|
||||
'Return Type',
|
||||
'Short Name'
|
||||
@ -36,9 +79,18 @@ class VariableTableWidget(QTableWidget):
|
||||
self.var_list = []
|
||||
|
||||
self.type_options = list(dict.fromkeys(type_map.values()))
|
||||
self.display_type_options = [t.replace('pt_', '') for t in self.type_options]
|
||||
self.iq_types = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
|
||||
self.pt_types_all = [t.replace('pt_', '') for t in self.type_options]
|
||||
self.iq_types_all = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
|
||||
# Задаём базовые iq-типы (без префикса 'iq_')
|
||||
self.iq_types = ['iq_none', 'iq24', 'iq19', 'iq15', 'iq10']
|
||||
# Фильтруем типы из type_map.values() исключая те, что содержат 'arr' или 'ptr'
|
||||
self.type_options = [t for t in set(type_map.values()) if 'arr' not in t and 'ptr' not in t]
|
||||
# Формируем display_type_options без префикса 'pt_'
|
||||
self.pt_types = [t.replace('pt_', '') for t in self.type_options]
|
||||
|
||||
self._iq_type_filter = list(self.iq_types) # Текущий фильтр iq типов (по умолчанию все)
|
||||
self._pt_type_filter = list(self.pt_types)
|
||||
self._ret_type_filter = list(self.iq_types)
|
||||
header = self.horizontalHeader()
|
||||
# Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину
|
||||
|
||||
@ -60,12 +112,13 @@ class VariableTableWidget(QTableWidget):
|
||||
self.setColumnWidth(rows.type, 100)
|
||||
self._resizing = False
|
||||
self.horizontalHeader().sectionResized.connect(self.on_section_resized)
|
||||
self.horizontalHeader().sectionClicked.connect(self.on_header_clicked)
|
||||
|
||||
|
||||
def populate(self, vars_list, structs, on_change_callback):
|
||||
self.var_list = vars_list
|
||||
self.type_options = list(dict.fromkeys(type_map.values()))
|
||||
self.display_type_options = [t.replace('pt_', '') for t in self.type_options]
|
||||
self.pt_types = [t.replace('pt_', '') for t in self.type_options]
|
||||
iq_types = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
|
||||
|
||||
# --- ДО: удаляем отображение структур и union-переменных
|
||||
@ -116,9 +169,9 @@ class VariableTableWidget(QTableWidget):
|
||||
|
||||
# pt_type
|
||||
pt_combo = QComboBox()
|
||||
pt_combo.addItems(self.display_type_options)
|
||||
pt_combo.addItems(self.pt_types)
|
||||
value = var['pt_type'].replace('pt_', '')
|
||||
if value not in self.display_type_options:
|
||||
if value not in self.pt_types:
|
||||
pt_combo.addItem(value)
|
||||
pt_combo.setCurrentText(value)
|
||||
pt_combo.currentTextChanged.connect(on_change_callback)
|
||||
@ -135,14 +188,17 @@ class VariableTableWidget(QTableWidget):
|
||||
iq_combo.setCurrentText(value)
|
||||
iq_combo.currentTextChanged.connect(on_change_callback)
|
||||
iq_combo.setStyleSheet(style_with_padding)
|
||||
iq_combo.wheelEvent = lambda e: e.ignore()
|
||||
self.setCellWidget(row, rows.iq_type, iq_combo)
|
||||
|
||||
# return_type
|
||||
ret_combo = QComboBox()
|
||||
ret_combo.addItems(self.iq_types)
|
||||
ret_combo.setCurrentText(var.get('return_type', ''))
|
||||
value = var['return_type'].replace('t_', '')
|
||||
ret_combo.setCurrentText(value)
|
||||
ret_combo.currentTextChanged.connect(on_change_callback)
|
||||
ret_combo.setStyleSheet(style_with_padding)
|
||||
ret_combo.wheelEvent = lambda e: e.ignore()
|
||||
self.setCellWidget(row, rows.ret_type, ret_combo)
|
||||
|
||||
# short_name
|
||||
@ -155,25 +211,28 @@ class VariableTableWidget(QTableWidget):
|
||||
self.check()
|
||||
|
||||
def check(self):
|
||||
warning_color = (QColor("#FFFACD")) # Жёлтый для длинных shortname
|
||||
error_color = (QColor("#FFB6C1")) # Светло-красный для отсутствующих переменных
|
||||
warning_color = QColor("#FFFACD")
|
||||
error_color = QColor("#FFB6C1")
|
||||
tooltip_shortname = "Short Name длиннее 10 символов — будет обрезано при генерации"
|
||||
tooltip_missing = f'Имя переменной не найдено среди переменных. Добавьте её через кнопку "Добавить переменные"'
|
||||
tooltip_missing = 'Имя переменной не найдено среди переменных. Добавьте её через кнопку "Добавить переменные"'
|
||||
|
||||
var_names_set = {v.get('name') for v in self.var_list if v.get('name')}
|
||||
|
||||
t0 = time.time()
|
||||
self.setUpdatesEnabled(False)
|
||||
for row in range(self.rowCount()):
|
||||
# Получаем имя переменной (столбец `name`)
|
||||
t1 = time.time()
|
||||
name_widget = self.cellWidget(row, rows.name)
|
||||
t2 = time.time()
|
||||
name = name_widget.text() if name_widget else ""
|
||||
|
||||
# Получаем shortname
|
||||
short_name_edit = self.cellWidget(row, rows.short_name)
|
||||
t3 = time.time()
|
||||
shortname = short_name_edit.text() if short_name_edit else ""
|
||||
|
||||
# Флаги ошибок
|
||||
long_shortname = len(shortname) > 10
|
||||
found = any(v.get('name') == name for v in self.var_list)
|
||||
|
||||
# Выбираем цвет и подсказку
|
||||
found = name in var_names_set
|
||||
|
||||
color = None
|
||||
tooltip = ""
|
||||
if not found:
|
||||
@ -184,6 +243,10 @@ class VariableTableWidget(QTableWidget):
|
||||
tooltip = tooltip_shortname
|
||||
|
||||
self.highlight_row(row, color, tooltip)
|
||||
t4 = time.time()
|
||||
|
||||
self.setUpdatesEnabled(True)
|
||||
#print(f"Row {row}: cellWidget(name) {t2-t1:.4f}s, cellWidget(shortname) {t3-t2:.4f}s, highlight_row {t4-t3:.4f}s")
|
||||
|
||||
|
||||
def read_data(self):
|
||||
@ -209,6 +272,52 @@ class VariableTableWidget(QTableWidget):
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def on_header_clicked(self, logicalIndex):
|
||||
if logicalIndex == rows.pt_type:
|
||||
dlg = FilterDialog(self, self.pt_types_all, self._pt_type_filter, "Выберите Pointer Types")
|
||||
if dlg.exec_():
|
||||
self._pt_type_filter = dlg.get_selected()
|
||||
self.update_comboboxes({rows.pt_type: self._pt_type_filter})
|
||||
|
||||
elif logicalIndex == rows.iq_type:
|
||||
dlg = FilterDialog(self, self.iq_types_all, self._iq_type_filter, "Выберите IQ типы")
|
||||
if dlg.exec_():
|
||||
self._iq_type_filter = dlg.get_selected()
|
||||
self.update_comboboxes({rows.iq_type: self._iq_type_filter})
|
||||
|
||||
elif logicalIndex == rows.ret_type:
|
||||
dlg = FilterDialog(self, self.iq_types_all, self._ret_type_filter, "Выберите IQ типы")
|
||||
if dlg.exec_():
|
||||
self._ret_type_filter = dlg.get_selected()
|
||||
self.update_comboboxes({rows.ret_type: self._ret_type_filter})
|
||||
|
||||
|
||||
|
||||
def update_comboboxes(self, columns_filters: Dict[int, List[str]]):
|
||||
"""
|
||||
Обновляет combobox-ячейки в указанных столбцах таблицы.
|
||||
|
||||
:param columns_filters: dict, где ключ — индекс столбца,
|
||||
значение — список допустимых вариантов для combobox.
|
||||
"""
|
||||
for row in range(self.rowCount()):
|
||||
for col, allowed_items in columns_filters.items():
|
||||
combo = self.cellWidget(row, col)
|
||||
if combo:
|
||||
current = combo.currentText()
|
||||
combo.blockSignals(True)
|
||||
combo.clear()
|
||||
combo.addItems(allowed_items)
|
||||
if current in allowed_items:
|
||||
combo.setCurrentText(current)
|
||||
else:
|
||||
combo.setCurrentIndex(0)
|
||||
combo.blockSignals(False)
|
||||
|
||||
|
||||
|
||||
def on_section_resized(self, logicalIndex, oldSize, newSize):
|
||||
if self._resizing:
|
||||
return # предотвращаем рекурсию
|
||||
@ -256,20 +365,22 @@ class VariableTableWidget(QTableWidget):
|
||||
item = self.item(row, col)
|
||||
widget = self.cellWidget(row, col)
|
||||
|
||||
# Подсветка обычной item-ячейки (например, тип переменной)
|
||||
if item is not None:
|
||||
if color:
|
||||
current_bg = item.background().color() if item.background() else None
|
||||
if color and current_bg != color:
|
||||
item.setBackground(QBrush(color))
|
||||
item.setToolTip(tooltip)
|
||||
else:
|
||||
elif not color and current_bg is not None:
|
||||
item.setBackground(QBrush(Qt.NoBrush))
|
||||
item.setToolTip("")
|
||||
|
||||
# Подсветка виджетов — здесь главная доработка
|
||||
elif widget is not None:
|
||||
# Надёжная подсветка: через styleSheet
|
||||
widget.setStyleSheet(css_color)
|
||||
widget.setToolTip(tooltip if color else "")
|
||||
if widget.styleSheet() != css_color:
|
||||
widget.setStyleSheet(css_color)
|
||||
current_tip = widget.toolTip()
|
||||
if color and current_tip != tooltip:
|
||||
widget.setToolTip(tooltip)
|
||||
elif not color and current_tip:
|
||||
widget.setToolTip("")
|
||||
|
||||
def get_selected_var_names(self):
|
||||
selected_indexes = self.selectedIndexes()
|
||||
|
@ -6,7 +6,7 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
import lxml.etree as ET
|
||||
from pathlib import Path
|
||||
from xml.dom import minidom
|
||||
import myXML
|
||||
|
36
Src/myXML.py
36
Src/myXML.py
@ -1,5 +1,5 @@
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
from lxml import etree
|
||||
|
||||
def make_absolute_path(path, base_path):
|
||||
if not os.path.isabs(path) and os.path.isdir(base_path):
|
||||
@ -33,44 +33,38 @@ def make_relative_path(abs_path, base_path):
|
||||
return abs_path.replace("\\", "/")
|
||||
|
||||
def indent_xml(elem, level=0):
|
||||
indent = " "
|
||||
i = "\n" + level * indent
|
||||
# Убираем strip() — они медленные, и текст мы всё равно перезаписываем
|
||||
i = "\n" + level * " "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + indent
|
||||
elem.text = elem.text or i + " "
|
||||
for child in elem:
|
||||
indent_xml(child, level + 1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
elem[-1].tail = elem[-1].tail or i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
elem.tail = elem.tail or i
|
||||
|
||||
|
||||
def fwrite(root, xml_full_path):
|
||||
indent_xml(root)
|
||||
ET.ElementTree(root).write(xml_full_path, encoding="utf-8", xml_declaration=True)
|
||||
#indent_xml(root)
|
||||
#ET.ElementTree(root).write(xml_full_path, encoding="utf-8", xml_declaration=True)
|
||||
rough_string = etree.tostring(root, encoding="utf-8")
|
||||
parsed = etree.fromstring(rough_string)
|
||||
with open(xml_full_path, "wb") as f:
|
||||
f.write(etree.tostring(parsed, pretty_print=True, encoding="utf-8", xml_declaration=True))
|
||||
|
||||
|
||||
def safe_parse_xml(xml_path):
|
||||
"""
|
||||
Безопасно парсит XML-файл.
|
||||
|
||||
Возвращает кортеж (root, tree) или (None, None) при ошибках.
|
||||
"""
|
||||
def safe_parse_xml(xml_path):
|
||||
if not xml_path or not os.path.isfile(xml_path):
|
||||
print(f"Файл '{xml_path}' не найден или путь пустой")
|
||||
return None, None
|
||||
|
||||
try:
|
||||
if os.path.getsize(xml_path) == 0:
|
||||
return None, None
|
||||
|
||||
tree = ET.parse(xml_path)
|
||||
tree = etree.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
return root, tree
|
||||
|
||||
except ET.ParseError as e:
|
||||
except etree .XMLSyntaxError as e:
|
||||
print(f"Ошибка парсинга XML файла '{xml_path}': {e}")
|
||||
return None, None
|
||||
except Exception as e:
|
||||
|
@ -9,7 +9,7 @@ import re
|
||||
import clang.cindex
|
||||
from clang import cindex
|
||||
from clang.cindex import Config
|
||||
import xml.etree.ElementTree as ET
|
||||
import lxml.etree as ET
|
||||
from xml.dom import minidom
|
||||
from parseMakefile import parse_makefile
|
||||
from collections import deque
|
||||
@ -545,7 +545,7 @@ def read_vars_from_xml(xml_path):
|
||||
'shortname': var_elem.findtext('shortname', name),
|
||||
'pt_type': var_elem.findtext('pt_type', ''),
|
||||
'iq_type': var_elem.findtext('iq_type', ''),
|
||||
'return_type': var_elem.findtext('return_type', 'pt_iq_none'),
|
||||
'return_type': var_elem.findtext('return_type', 't_iq_none'),
|
||||
'type': var_elem.findtext('type', 'unknown'),
|
||||
'file': var_elem.findtext('file', ''),
|
||||
'extern': get_bool('extern'),
|
||||
@ -608,7 +608,7 @@ def generate_xml_output(proj_path, xml_path, unique_vars, h_files_needed, vars_n
|
||||
'shortname': info.get('shortname', name),
|
||||
'pt_type': info.get('pt_type', ''),
|
||||
'iq_type': info.get('iq_type', ''),
|
||||
'return_type': info.get('return_type', 'pt_iq_none'),
|
||||
'return_type': info.get('return_type', 't_iq_none'),
|
||||
'type': info.get('type', 'unknown'),
|
||||
'file': info.get('file', ''),
|
||||
'extern': info.get('extern', False),
|
||||
@ -627,7 +627,7 @@ def generate_xml_output(proj_path, xml_path, unique_vars, h_files_needed, vars_n
|
||||
ET.SubElement(var_elem, "shortname").text = info.get('shortname', name)
|
||||
ET.SubElement(var_elem, "pt_type").text = info.get('pt_type', '')
|
||||
ET.SubElement(var_elem, "iq_type").text = info.get('iq_type', '')
|
||||
ET.SubElement(var_elem, "return_type").text = info.get('return_type', 'pt_iq_none')
|
||||
ET.SubElement(var_elem, "return_type").text = info.get('return_type', 't_iq_none')
|
||||
|
||||
ET.SubElement(var_elem, "type").text = info.get('type', 'unknown')
|
||||
rel_file = make_relative_if_possible(info.get('file', ''), proj_path).replace("\\", "/")
|
||||
|
@ -63,82 +63,8 @@ def split_path(path):
|
||||
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
|
||||
|
||||
def is_lazy_item(item):
|
||||
return item.childCount() == 1 and item.child(0).text(0) == 'lazy_marker'
|
||||
|
||||
|
||||
class VariableSelectWidget(QWidget):
|
||||
@ -152,10 +78,10 @@ class VariableSelectWidget(QWidget):
|
||||
self._vars_hash = None
|
||||
|
||||
# --- UI Элементы ---
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input = QLineEdit(self)
|
||||
self.search_input.setPlaceholderText("Поиск...")
|
||||
|
||||
self.tree = QTreeWidget()
|
||||
self.tree = QTreeWidget(self)
|
||||
self.tree.setHeaderLabels(["Имя переменной", "Тип"])
|
||||
self.tree.setSelectionMode(QTreeWidget.ExtendedSelection)
|
||||
self.tree.setRootIsDecorated(True)
|
||||
@ -166,7 +92,7 @@ class VariableSelectWidget(QWidget):
|
||||
""")
|
||||
self.tree.itemExpanded.connect(self.on_item_expanded)
|
||||
|
||||
self.completer = QCompleter()
|
||||
self.completer = QCompleter(self)
|
||||
self.completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.completer.setFilterMode(Qt.MatchContains)
|
||||
@ -179,9 +105,10 @@ class VariableSelectWidget(QWidget):
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
# --- Соединения ---
|
||||
self.search_input.textChanged.connect(self.on_search_text_changed)
|
||||
#self.search_input.textChanged.connect(self.on_search_text_changed)
|
||||
self.search_input.textChanged.connect(lambda text: self.on_search_text_changed(text))
|
||||
self.search_input.installEventFilter(self)
|
||||
self.completer.activated[str].connect(self.insert_completion)
|
||||
self.completer.activated[str].connect(lambda text: self.insert_completion(text))
|
||||
|
||||
# --- Публичные методы для управления виджетом снаружи ---
|
||||
|
||||
@ -221,7 +148,7 @@ class VariableSelectWidget(QWidget):
|
||||
self.tree.setColumnWidth(0, 400)
|
||||
|
||||
def on_item_expanded(self, item):
|
||||
if self.is_lazy_item(item):
|
||||
if is_lazy_item(item):
|
||||
item.removeChild(item.child(0))
|
||||
var = item.data(0, Qt.UserRole + 100)
|
||||
if var:
|
||||
@ -265,9 +192,59 @@ class VariableSelectWidget(QWidget):
|
||||
item.setData(0, Qt.UserRole + 100, var) # Сохраняем var целиком
|
||||
|
||||
|
||||
def show_matching_path(self, 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
|
||||
self.on_item_expanded(item)
|
||||
for i in range(item.childCount()):
|
||||
child = item.child(i)
|
||||
if self.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
|
||||
|
||||
|
||||
def filter_tree(self):
|
||||
text = self.search_input.text().strip().lower()
|
||||
print(f"[{self.objectName()}] Filtering tree with text: '{text}'")
|
||||
path_parts = split_path(text) if text else []
|
||||
|
||||
if '.' not in text and '->' not in text and '[' not in text and text != '':
|
||||
@ -282,7 +259,7 @@ class VariableSelectWidget(QWidget):
|
||||
else:
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
item = self.tree.topLevelItem(i)
|
||||
show_matching_path(item, path_parts, 0)
|
||||
self.show_matching_path(item, path_parts, 0)
|
||||
|
||||
|
||||
|
||||
@ -381,6 +358,8 @@ class VariableSelectWidget(QWidget):
|
||||
|
||||
self.completer.setModel(QStringListModel(completions))
|
||||
self.completer.complete()
|
||||
print(f"[{self.objectName()}] Updating completions for text: '{text}' -> {len(completions)} completions found.")
|
||||
print(f" Completions: {completions[:5]}") # Вывести первые 5 для проверки
|
||||
return completions
|
||||
|
||||
|
||||
@ -487,7 +466,12 @@ class VariableSelectWidget(QWidget):
|
||||
self.completer.complete()
|
||||
return True
|
||||
|
||||
def on_search_text_changed(self, text):
|
||||
def on_search_text_changed(self, text):
|
||||
sender_widget = self.sender()
|
||||
sender_name = sender_widget.objectName() if sender_widget else "Unknown Sender"
|
||||
print(f"[{self.objectName()}] (Sender: {sender_name}) Search text changed: '{text}'")
|
||||
|
||||
self.completer.setWidget(self.search_input)
|
||||
self.filter_tree()
|
||||
if text == None:
|
||||
text = self.search_input.text().strip()
|
||||
@ -500,6 +484,18 @@ class VariableSelectWidget(QWidget):
|
||||
else:
|
||||
self.completer.popup().hide()
|
||||
|
||||
def focusInEvent(self, event):
|
||||
if self.completer.widget() != self.search_input:
|
||||
self.completer.setWidget(self.search_input)
|
||||
super().focusInEvent(event)
|
||||
|
||||
def _custom_focus_in_event(self, event):
|
||||
# Принудительно установить виджет для completer при получении фокуса
|
||||
if self.completer.widget() != self.search_input:
|
||||
self.completer.setWidget(self.search_input)
|
||||
super(QLineEdit, self.search_input).focusInEvent(event) # Вызвать оригинальный обработчик
|
||||
|
||||
|
||||
def build_completion_list(self):
|
||||
completions = []
|
||||
|
||||
@ -593,9 +589,6 @@ class VariableSelectWidget(QWidget):
|
||||
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) переменных, исключая битовые поля и группы."""
|
||||
@ -611,3 +604,7 @@ class VariableSelectWidget(QWidget):
|
||||
"""Возвращает имена только конечных (leaf) переменных из выделенных."""
|
||||
return [item.text(0) for item in self.get_selected_items() if item.text(0)]
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.completer.setWidget(None)
|
||||
self.completer.deleteLater()
|
||||
super().closeEvent(event)
|
@ -1,7 +1,7 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
import lxml.etree as ET
|
||||
from generateVars import map_type_to_pt, get_iq_define, type_map
|
||||
from enum import IntEnum
|
||||
import scanVars
|
||||
@ -138,7 +138,7 @@ def parse_vars(filename, typedef_map=None):
|
||||
'shortname': var.findtext('shortname', name),
|
||||
'pt_type': pt_type,
|
||||
'iq_type': iq_type,
|
||||
'return_type': var.findtext('return_type', ''),
|
||||
'return_type': var.findtext('return_type', 't_iq_none'),
|
||||
'type': var_type,
|
||||
'file': var.findtext('file', ''),
|
||||
'extern': var.findtext('extern', 'false') == 'true',
|
||||
|
@ -2,13 +2,11 @@ import subprocess
|
||||
import shutil
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image # нужна библиотека Pillow для конвертации PNG в ICO
|
||||
import PySide2
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
# install:
|
||||
# install: pip install PySide2 lxml nuitka pyinstaller
|
||||
# - PyInstaller
|
||||
# - nuitka
|
||||
# - Pillow
|
||||
# - PySide2
|
||||
# - clang
|
||||
|
||||
@ -25,6 +23,11 @@ SPEC_PATH = WORK_PATH
|
||||
ICON_PATH = SRC_PATH / "icon.png"
|
||||
ICON_ICO_PATH = SRC_PATH / "icon.ico"
|
||||
|
||||
TEMP_FOLDERS = [
|
||||
"build_temp",
|
||||
"__pycache__",
|
||||
"DebugVarEdit_GUI\..*$"
|
||||
]
|
||||
# === Пути к DLL и прочим зависимостям ===
|
||||
LIBS = {
|
||||
"libclang.dll": SRC_PATH / "libclang.dll"
|
||||
@ -48,7 +51,7 @@ for name, path in LIBS.items():
|
||||
print(f"WARNING: {path.name} не найден — он не будет включён в сборку")
|
||||
|
||||
def clean_temp():
|
||||
for folder in ["build_temp", "__pycache__", "DebugVarEdit_GUI.build", "DebugVarEdit_GUI.dist", "DebugVarEdit_GUI.onefile-build"]:
|
||||
for folder in TEMP_FOLDERS:
|
||||
path = Path(folder)
|
||||
if path.exists():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
31780
structs.xml
31780
structs.xml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user