from PySide2.QtWidgets import ( QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QCompleter, QAbstractItemView, QHeaderView, QLabel, QSpacerItem, QSizePolicy, QSpinBox, QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget ) from PySide2.QtGui import QColor, QBrush, QPalette from PySide2.QtCore import Qt, QSettings from enum import IntEnum from generate_debug_vars import type_map import time from typing import Dict, List class rows(IntEnum): No = 0 include = 1 name = 2 type = 3 pt_type = 4 iq_type = 5 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 SetSizeDialog(QDialog): """ Диалоговое окно для выбора числового значения (размера). """ def __init__(self, parent=None, initial_value=10, min_value=1, max_value=50, title="Укажите размер короткого имени"): super().__init__(parent) self.setWindowTitle(title) self.setFixedSize(320, 120) # Задаем фиксированный размер для аккуратного вида # Основной вертикальный макет main_layout = QVBoxLayout(self) # Макет для ввода значения input_layout = QHBoxLayout() label = QLabel("Количество символов:", self) self.spin_box = QSpinBox(self) self.spin_box.setRange(min_value, max_value) # Устанавливаем диапазон допустимых значений initial_value = parent._shortname_size self.spin_box.setValue(initial_value) # Устанавливаем начальное значение self.spin_box.setFocus() # Устанавливаем фокус на поле ввода input_layout.addWidget(label) input_layout.addWidget(self.spin_box) main_layout.addLayout(input_layout) # Добавляем пустое пространство для лучшего разделения main_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)) # Макет для кнопок btn_layout = QHBoxLayout() btn_layout.addStretch() # Добавляем растягивающийся элемент, чтобы кнопки были справа btn_ok = QPushButton("OK") btn_cancel = QPushButton("Отмена") btn_layout.addWidget(btn_ok) btn_layout.addWidget(btn_cancel) main_layout.addLayout(btn_layout) # Подключение сигналов к слотам btn_ok.clicked.connect(self.accept) # При нажатии "OK" диалог закроется со статусом "Accepted" btn_cancel.clicked.connect(self.reject) # При нажатии "Отмена" - со статусом "Rejected" def get_selected_size(self): """ Возвращает значение, выбранное в QSpinBox. """ return self.spin_box.value() class CtrlScrollComboBox(QComboBox): def wheelEvent(self, event): if event.modifiers() & Qt.ControlModifier: super().wheelEvent(event) else: event.ignore() class VariableTableWidget(QTableWidget): def __init__(self, parent=None): super().__init__(0, 8, parent) # Таблица переменных self.setHorizontalHeaderLabels([ '№', # новый столбец 'En', 'Name', 'Origin Type', 'Base Type', 'IQ Type', 'Return Type', 'Short Name' ]) self.setEditTriggers(QAbstractItemView.AllEditTriggers) self.var_list = [] # Инициализируем QSettings с именем организации и приложения self.settings = QSettings("SET", "DebugVarEdit_VarTable") # Восстанавливаем сохранённое состояние, если есть shortsize = self.settings.value("shortname_size", True, type=int) self._shortname_size = shortsize self.type_options = list(dict.fromkeys(type_map.values())) 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', 'iq', 'iq10', 'iq15', 'iq19', 'iq24'] # Фильтруем типы из type_map.values() исключая те, что содержат 'arr' или 'ptr' type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t and 'struct' not in t and 'union' not in t and '64' not in t] # Формируем display_type_options без префикса 'pt_' self.pt_types = [t.replace('pt_', '') for t in 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), чтобы они заняли всю оставшуюся ширину for col in range(self.columnCount()): if col == self.columnCount() - 1: header.setSectionResizeMode(col, QHeaderView.Stretch) else: header.setSectionResizeMode(col, QHeaderView.Interactive) parent_widget = self.parentWidget() # Сделаем колонки с номерами фиксированной ширины self.setColumnWidth(rows.No, 30) self.setColumnWidth(rows.include, 30) self.setColumnWidth(rows.pt_type, 85) self.setColumnWidth(rows.iq_type, 85) self.setColumnWidth(rows.ret_type, 85) self.setColumnWidth(rows.name, 300) 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 # --- ДО: удаляем отображение структур и union-переменных for var in vars_list: pt_type = var.get('pt_type', '') if 'struct' in pt_type or 'union' in pt_type: var['show_var'] = 'false' var['enable'] = 'false' filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true'] self.setRowCount(len(filtered_vars)) self.verticalHeader().setVisible(False) style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';" for row, var in enumerate(filtered_vars): # № no_item = QTableWidgetItem(str(row)) no_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.setItem(row, rows.No, no_item) # Enable cb = QCheckBox() cb.setChecked(var.get('enable', 'false') == 'true') cb.stateChanged.connect(on_change_callback) cb.setStyleSheet(style_with_padding) self.setCellWidget(row, rows.include, cb) # Name name_edit = QLineEdit(var['name']) if var['type'] in structs: completer = QCompleter(structs[var['type']].keys()) completer.setCaseSensitivity(Qt.CaseInsensitive) name_edit.setCompleter(completer) name_edit.textChanged.connect(on_change_callback) name_edit.setStyleSheet(style_with_padding) self.setCellWidget(row, rows.name, name_edit) # Origin Type (readonly) origin_item = QTableWidgetItem(var.get('type', '')) origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) origin_item.setToolTip(var.get('type', '')) # Всплывающая подсказка origin_item.setForeground(QBrush(Qt.black)) self.setItem(row, rows.type, origin_item) # pt_type pt_combo = CtrlScrollComboBox() pt_combo.addItems(self.pt_types) value = var['pt_type'].replace('pt_', '') if value not in self.pt_types: pt_combo.addItem(value) pt_combo.setCurrentText(value) pt_combo.currentTextChanged.connect(on_change_callback) pt_combo.setStyleSheet(style_with_padding) self.setCellWidget(row, rows.pt_type, pt_combo) # iq_type iq_combo = CtrlScrollComboBox() iq_combo.addItems(self.iq_types) value = var['iq_type'].replace('t_', '') if value not in self.iq_types: iq_combo.addItem(value) iq_combo.setCurrentText(value) iq_combo.currentTextChanged.connect(on_change_callback) iq_combo.setStyleSheet(style_with_padding) self.setCellWidget(row, rows.iq_type, iq_combo) # return_type ret_combo = CtrlScrollComboBox() ret_combo.addItems(self.iq_types) value = var['return_type'].replace('t_', '') ret_combo.setCurrentText(value) ret_combo.currentTextChanged.connect(on_change_callback) ret_combo.setStyleSheet(style_with_padding) self.setCellWidget(row, rows.ret_type, ret_combo) # short_name short_name_val = var.get('shortname', var['name']) short_name_edit = QLineEdit(short_name_val) short_name_edit.textChanged.connect(on_change_callback) short_name_edit.setStyleSheet(style_with_padding) self.setCellWidget(row, rows.short_name, short_name_edit) self.check() def check(self): warning_color = QColor("#FFFACD") error_color = QColor("#FFB6C1") tooltip_shortname = "Short Name длиннее 10 символов — будет обрезано при генерации" 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()): t1 = time.time() name_widget = self.cellWidget(row, rows.name) t2 = time.time() name = name_widget.text() if name_widget else "" 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) > self._shortname_size found = name in var_names_set color = None tooltip = "" if not found: color = error_color tooltip = tooltip_missing elif long_shortname: color = warning_color 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): result = [] for row in range(self.rowCount()): cb = self.cellWidget(row, rows.include) name = self.cellWidget(row, rows.name).text() pt = self.cellWidget(row, rows.pt_type).currentText() iq = self.cellWidget(row, rows.iq_type).currentText() ret = self.cellWidget(row, rows.ret_type).currentText() shortname = self.cellWidget(row, rows.short_name).text() origin_type = self.item(row, rows.type).text() result.append({ 'show_var': True, 'enable': cb.isChecked(), 'name': name, 'pt_type': f'pt_{pt}', 'iq_type': f't_{iq}', 'return_type': f't_{ret}', 'shortname': shortname, 'type': origin_type, }) return result def on_header_clicked(self, logicalIndex): if logicalIndex == rows.pt_type: dlg = FilterDialog(self, self.pt_types_all, self._pt_type_filter, "Выберите базовые типы") 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}) elif logicalIndex == rows.short_name: dlg = SetSizeDialog(self) if dlg.exec_(): self._shortname_size = dlg.get_selected_size() self.settings.setValue("shortname_size", self._shortname_size) self.check() 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 # предотвращаем рекурсию min_width = 50 delta = newSize - oldSize right_index = logicalIndex + 1 if right_index >= self.columnCount(): # Если правая колока - нет соседа, ограничиваем минимальную ширину if newSize < min_width: self._resizing = True self.setColumnWidth(logicalIndex, min_width) self._resizing = False return self._resizing = True try: right_width = self.columnWidth(right_index) new_right_width = right_width - delta # Если соседняя колонка станет уже минимальной - подкорректируем левую if new_right_width < min_width: new_right_width = min_width newSize = oldSize + (right_width - min_width) self.setColumnWidth(logicalIndex, newSize) self.setColumnWidth(right_index, new_right_width) finally: self._resizing = False def highlight_row(self, row: int, color: QColor = None, tooltip: str = ""): """ Подсвечивает строку таблицы цветом `color`, не меняя шрифт. Работает с QLineEdit, QComboBox, QCheckBox (включая обёртки). Если `color=None`, сбрасывает подсветку. """ css_reset = "background-color: none; font: inherit;" css_color = f"background-color: {color.name()};" if color else css_reset for col in range(self.columnCount()): item = self.item(row, col) widget = self.cellWidget(row, col) if item is not None: current_bg = item.background().color() if item.background() else None if color and current_bg != color: item.setBackground(QBrush(color)) item.setToolTip(tooltip) elif not color and current_bg is not None: item.setBackground(QBrush(Qt.NoBrush)) item.setToolTip("") elif widget is not None: 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() selected_rows = set(index.row() for index in selected_indexes) names = [] for row in selected_rows: name_widget = self.cellWidget(row, rows.name) if name_widget: name = name_widget.text() if name: names.append(name) return names