import json import os import weakref from functools import partial from copy import deepcopy from PySide6.QtWidgets import * from PySide6.QtCore import * from PySide6.QtGui import * class _MacroActionIcon(QIconEngine): """Scalable action icons, painted with the button's current theme color.""" def __init__(self, action, button): super().__init__() self.action = action self.button = weakref.ref(button) def clone(self): return _MacroActionIcon(self.action, self.button()) def paint(self, painter, rect, mode, state): button = self.button() palette = button.palette() if button is not None else QApplication.palette() group = QPalette.Disabled if mode == QIcon.Disabled else QPalette.Active painter.save() painter.setRenderHint(QPainter.Antialiasing) side = min(rect.width(), rect.height()) painter.translate(rect.x() + (rect.width() - side) / 2, rect.y() + (rect.height() - side) / 2) painter.scale(side / 24, side / 24) painter.setPen(QPen(palette.color(group, QPalette.ButtonText), 1.8, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.setBrush(Qt.NoBrush) if self.action == "edit": path = QPainterPath(QPointF(4, 16)) for x, y in ((15, 5), (19, 9), (8, 20), (3, 21), (4, 16)): path.lineTo(x, y) painter.drawPath(path) painter.drawLine(QPointF(13, 7), QPointF(17, 11)) else: painter.drawLine(QPointF(4, 6), QPointF(20, 6)) painter.drawRoundedRect(QRectF(6, 6, 12, 15), 1.5, 1.5) painter.drawPolyline(QPolygonF([QPointF(9, 6), QPointF(9, 3), QPointF(15, 3), QPointF(15, 6)])) painter.drawLine(QPointF(10, 10), QPointF(10, 17)) painter.drawLine(QPointF(14, 10), QPointF(14, 17)) painter.restore() def pixmap(self, size, mode, state): pixmap = QPixmap(size) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) self.paint(painter, QRect(QPoint(), size), mode, state) painter.end() return pixmap class SerialBus: """ВЫСОКОУРОВНЕВЫЙ протокол для работы с серийной шиной (только через регистры)""" def __init__(self, raw_protocol): self.raw = raw_protocol # Используем низкоуровневый протокол def _execute(self, write, tetrad, register, data, device_address): from protocan.tms_terminal import bus_steps try: steps = bus_steps(write, tetrad, register, data) except (ValueError, RuntimeError) as error: return False, str(error) result = None for address, value in steps: success, result = (self.raw.peek(address, device_address) if value is None else self.raw.poke(address, value, device_address)) if not success: return False, result return True, "Запись в шину подтверждена" if write else result def write_bus(self, data_word, address_tetrad, register_num=0, device_address=1): return self._execute(True, address_tetrad, register_num, data_word, device_address) def read_bus(self, address_tetrad, register_num=0, device_address=1): return self._execute(False, address_tetrad, register_num, 0, device_address) class MacroCommandDialog(QDialog): """Диалог для добавления/редактирования команды макроса""" def __init__(self, parent=None, command=None): super().__init__(parent) self.command = command if command else {} self.init_ui() def init_ui(self): self.setWindowTitle("Команда макроса") self.setMinimumWidth(400) layout = QVBoxLayout() # Тип команды type_layout = QHBoxLayout() type_layout.addWidget(QLabel("Тип команды:")) self.cmd_type = QComboBox() self.cmd_type.addItems(["Запись", "Чтение"]) type_layout.addWidget(self.cmd_type) layout.addLayout(type_layout) # Адрес устройства addr_layout = QHBoxLayout() addr_layout.addWidget(QLabel("Адрес устройства (0-14):")) self.device_addr = QSpinBox() self.device_addr.setRange(0, 14) addr_layout.addWidget(self.device_addr) layout.addLayout(addr_layout) # Регистр reg_layout = QHBoxLayout() reg_layout.addWidget(QLabel("Регистр (0-15):")) self.register_num = QSpinBox() self.register_num.setRange(0, 15) reg_layout.addWidget(self.register_num) layout.addLayout(reg_layout) # Данные (только для записи) self.data_label = QLabel("Данные (hex):") self.data_edit = QLineEdit("0000") self.data_edit.setMaximumWidth(100) data_layout = QHBoxLayout() data_layout.addWidget(self.data_label) data_layout.addWidget(self.data_edit) layout.addLayout(data_layout) # Загружаем данные команды, если она передана if self.command: if self.command.get('type') == 'write': self.cmd_type.setCurrentText("Запись") self.device_addr.setValue(self.command.get('tetrad', 0)) self.register_num.setValue(self.command.get('register', 0)) self.data_edit.setText(f"{self.command.get('data', 0):04X}") else: self.cmd_type.setCurrentText("Чтение") self.device_addr.setValue(self.command.get('tetrad', 0)) self.register_num.setValue(self.command.get('register', 0)) # Подключаем изменение типа команды self.cmd_type.currentTextChanged.connect(self.on_cmd_type_changed) self.on_cmd_type_changed(self.cmd_type.currentText()) # Кнопки button_layout = QHBoxLayout() self.ok_btn = QPushButton("OK") self.ok_btn.clicked.connect(self._accept_command) self.cancel_btn = QPushButton("Отмена") self.cancel_btn.clicked.connect(self.reject) button_layout.addWidget(self.ok_btn) button_layout.addWidget(self.cancel_btn) layout.addLayout(button_layout) self.setLayout(layout) def on_cmd_type_changed(self, text): """Показать/скрыть поле данных в зависимости от типа команды""" if text == "Запись": self.data_label.show() self.data_edit.show() else: self.data_label.hide() self.data_edit.hide() def _accept_command(self): if self.cmd_type.currentText() == "Запись": try: value = int(self.data_edit.text(), 16) if not 0 <= value <= 0xFFFF: raise ValueError() except ValueError: QMessageBox.warning(self, "Ошибка", "Данные должны быть в диапазоне 0000…FFFF") return self.accept() def get_command(self): """Получить команду из диалога""" cmd_type = 'write' if self.cmd_type.currentText() == "Запись" else 'read' command = { 'type': cmd_type, 'tetrad': self.device_addr.value(), 'register': self.register_num.value() } if cmd_type == 'write': try: command['data'] = int(self.data_edit.text(), 16) except ValueError: command['data'] = 0 return command class MacroEditorDialog(QDialog): """Диалог для создания/редактирования макроса""" def __init__(self, parent=None, macro=None): super().__init__(parent) self.macro = deepcopy(macro) if macro else {'name': '', 'commands': []} self.init_ui() def init_ui(self): self.setWindowTitle("Редактор макроса") self.setMinimumSize(500, 400) layout = QVBoxLayout() # Имя макроса name_layout = QHBoxLayout() name_layout.addWidget(QLabel("Имя макроса:")) self.macro_name = QLineEdit(self.macro.get('name', 'Новый макрос')) name_layout.addWidget(self.macro_name) layout.addLayout(name_layout) # Список команд layout.addWidget(QLabel("Команды:")) self.commands_list = QListWidget() layout.addWidget(self.commands_list) # Кнопки управления командами cmd_buttons_layout = QHBoxLayout() self.add_cmd_btn = QPushButton("Добавить команду") self.add_cmd_btn.clicked.connect(self.add_command) cmd_buttons_layout.addWidget(self.add_cmd_btn) self.edit_cmd_btn = QPushButton("Редактировать") self.edit_cmd_btn.clicked.connect(self.edit_command) cmd_buttons_layout.addWidget(self.edit_cmd_btn) self.remove_cmd_btn = QPushButton("Удалить") self.remove_cmd_btn.clicked.connect(self.remove_command) cmd_buttons_layout.addWidget(self.remove_cmd_btn) self.move_up_btn = QPushButton("Вверх") self.move_up_btn.clicked.connect(self.move_command_up) cmd_buttons_layout.addWidget(self.move_up_btn) self.move_down_btn = QPushButton("Вниз") self.move_down_btn.clicked.connect(self.move_command_down) cmd_buttons_layout.addWidget(self.move_down_btn) layout.addLayout(cmd_buttons_layout) # Загружаем команды, если они есть self.load_commands() # Кнопки сохранения/отмены button_layout = QHBoxLayout() self.save_btn = QPushButton("Сохранить") self.save_btn.clicked.connect(self.accept) self.cancel_btn = QPushButton("Отмена") self.cancel_btn.clicked.connect(self.reject) button_layout.addWidget(self.save_btn) button_layout.addWidget(self.cancel_btn) layout.addLayout(button_layout) self.setLayout(layout) def load_commands(self): """Загрузить команды в список""" self.commands_list.clear() for cmd in self.macro['commands']: if cmd['type'] == 'write': text = f"Запись: устр.{cmd['tetrad']}, рег.{cmd['register']}, 0x{cmd['data']:04X}" else: text = f"Чтение: устр.{cmd['tetrad']}, рег.{cmd['register']}" self.commands_list.addItem(text) def add_command(self): """Добавить новую команду""" dialog = MacroCommandDialog(self) if dialog.exec(): command = dialog.get_command() self.macro['commands'].append(command) self.load_commands() def edit_command(self): """Редактировать выбранную команду""" current_row = self.commands_list.currentRow() if current_row >= 0 and current_row < len(self.macro['commands']): dialog = MacroCommandDialog(self, self.macro['commands'][current_row]) if dialog.exec(): self.macro['commands'][current_row] = dialog.get_command() self.load_commands() def remove_command(self): """Удалить выбранную команду""" current_row = self.commands_list.currentRow() if current_row >= 0 and current_row < len(self.macro['commands']): del self.macro['commands'][current_row] self.load_commands() def move_command_up(self): """Переместить команду вверх""" current_row = self.commands_list.currentRow() if current_row > 0: self.macro['commands'][current_row], self.macro['commands'][current_row-1] = \ self.macro['commands'][current_row-1], self.macro['commands'][current_row] self.load_commands() self.commands_list.setCurrentRow(current_row-1) def move_command_down(self): """Переместить команду вниз""" current_row = self.commands_list.currentRow() if current_row >= 0 and current_row < len(self.macro['commands']) - 1: self.macro['commands'][current_row], self.macro['commands'][current_row+1] = \ self.macro['commands'][current_row+1], self.macro['commands'][current_row] self.load_commands() self.commands_list.setCurrentRow(current_row+1) def get_macro(self): """Получить макрос из диалога""" return { 'name': self.macro_name.text(), 'commands': self.macro['commands'].copy() } class SerialTab(QWidget): """Вкладка для работы с серийной шиной (ТОЛЬКО операции с шиной)""" MACROS_FILE = "macros.json" # Файл для сохранения макросов def __init__(self, serial_bus, raw_protocol, raw_widget): super().__init__() self.serial_bus = serial_bus self.raw_protocol = raw_protocol self.raw_widget = raw_widget self.macros = [] # Список макросов self.macro_buttons = [] # Список кнопок макросов self._macro_running = False self._macro_generation = 0 from pathlib import Path self.MACROS_FILE = str(Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) / "pysetterminal" / "macros.json") Path(self.MACROS_FILE).parent.mkdir(parents=True, exist_ok=True) # Загружаем сохраненные макросы self.load_macros() self.init_ui() def init_ui(self): layout = QVBoxLayout() # Группа для операций с шиной bus_group = QGroupBox("Операции с шиной (через регистры 0x200A/0x200B/0x200F)") bus_layout = QGridLayout() bus_layout.addWidget(QLabel("Данные для шины (hex):"), 0, 0) self.bus_data_edit = QLineEdit("0000") self.bus_data_edit.setMaximumWidth(100) bus_layout.addWidget(self.bus_data_edit, 0, 1) bus_layout.addWidget(QLabel("Адреса (0-14):"), 0, 2) self.bus_tetrad_edit = QLineEdit("0") self.bus_tetrad_edit.setMaximumWidth(50) bus_layout.addWidget(self.bus_tetrad_edit, 0, 3) bus_layout.addWidget(QLabel("Регистр (0-15):"), 0, 4) self.bus_register_edit = QLineEdit("0") self.bus_register_edit.setMaximumWidth(50) bus_layout.addWidget(self.bus_register_edit, 0, 5) self.bus_write_btn = QPushButton("Записать в шину") self.bus_write_btn.clicked.connect(self.write_to_bus) self.bus_read_btn = QPushButton("Прочитать из шину") self.bus_read_btn.clicked.connect(self.read_from_bus) bus_buttons = QHBoxLayout() bus_buttons.setSpacing(6) bus_buttons.addWidget(self.bus_write_btn) bus_buttons.addWidget(self.bus_read_btn) bus_buttons.addStretch(1) bus_layout.addLayout(bus_buttons, 1, 0, 1, 6) bus_layout.setColumnStretch(6, 1) bus_group.setLayout(bus_layout) layout.addWidget(bus_group) # --- Группа для макросов --- macros_group = QGroupBox("Макросы") macros_layout = QVBoxLayout() # Кнопка создания нового макроса self.create_macro_btn = QPushButton("Создать новый макрос") self.create_macro_btn.clicked.connect(self.create_macro) self.stop_macro_btn = QPushButton("Остановить макрос") self.stop_macro_btn.setEnabled(False) self.stop_macro_btn.clicked.connect(self.stop_macro) macro_actions = QHBoxLayout() macro_actions.setSpacing(6) macro_actions.addWidget(self.create_macro_btn) macro_actions.addWidget(self.stop_macro_btn) macro_actions.addStretch(1) macros_layout.addLayout(macro_actions) for button in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn, self.stop_macro_btn): button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) button.setFixedHeight(30) button.setStyleSheet("QPushButton { padding: 3px 10px; }") # Прокручиваемая область для кнопок макросов self.macros_scroll_area = QScrollArea() self.macros_scroll_area.setWidgetResizable(True) # Устанавливаем фиксированную высоту, чтобы вместить 3-4 строки макросов self.macros_scroll_area.setMinimumHeight(120) # Высота для 3-4 строк self.macros_scroll_content = QWidget() self.macros_scroll_layout = QGridLayout() self.macros_scroll_content.setLayout(self.macros_scroll_layout) self.macros_scroll_area.setWidget(self.macros_scroll_content) self.macros_scroll_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.macros_scroll_layout.setContentsMargins(6, 6, 6, 6) self.macros_scroll_layout.setHorizontalSpacing(8) self.macros_scroll_layout.setVerticalSpacing(6) macros_layout.addWidget(self.macros_scroll_area) macros_group.setLayout(macros_layout) layout.addWidget(macros_group) # --- ВНЕ ГРУППЫ: Результаты чтения --- results_layout = QGridLayout() # Считанное слово results_layout.addWidget(QLabel("Считанное слово:"), 0, 0) self.bus_read_value_label = QLabel("---") self.bus_read_value_label.setStyleSheet("font-weight: bold; font-size: 14px; border: 1px solid #ccc; padding: 5px;") results_layout.addWidget(self.bus_read_value_label, 0, 1) # Ответ контроллера results_layout.addWidget(QLabel("Ответ контроллера:"), 1, 0) self.bus_result_label = QLabel("---") self.bus_result_label.setStyleSheet("border: 1px solid #ccc; padding: 5px; font-family: monospace;") self.bus_result_label.setWordWrap(True) results_layout.addWidget(self.bus_result_label, 1, 1) # Создаем фрейм для результатов results_layout.setColumnStretch(2, 1) self.bus_read_value_label.setMinimumWidth(180) self.bus_result_label.setMaximumWidth(640) results_frame = QFrame() results_frame.setFrameShape(QFrame.StyledPanel) results_frame.setLayout(results_layout) layout.addWidget(results_frame) layout.addStretch() self.setLayout(layout) # После создания интерфейса создаем кнопки для загруженных макросов # ВАЖНО: Используем enumerate для правильных индексов for i, macro in enumerate(self.macros): self.add_macro_button_with_index(macro, i) def write_to_bus(self): """Запись в шину через регистры""" try: device_addr = 10 data = int(self.bus_data_edit.text(), 16) tetrad = int(self.bus_tetrad_edit.text()) register_num = int(self.bus_register_edit.text()) success, result = self.serial_bus.write_bus(data, tetrad, register_num, device_addr) if success: self.raw_widget.append_text(f"Запись в шину: адрес={tetrad}, рег={register_num}, данные=0x{data:04X} - OK") self.last_result = f"Запись в шину: данные=0x{data:04X}, адрес=0x{tetrad:X}, регистр=0x{register_num:X}" self.last_result += f"\n{result}" else: self.raw_widget.append_text(f"Запись в шину: адрес={tetrad}, рег={register_num} - Ошибка: {result}") self.last_result = f"Ошибка записи в шину: {result}" except ValueError as e: self.raw_widget.append_text(f"Ошибка в данных: {e}") self.last_result = f"Ошибка в данных: {e}" def read_from_bus(self): """Чтение из шины через регистры""" try: device_addr = 10 tetrad = int(self.bus_tetrad_edit.text()) register_num = int(self.bus_register_edit.text()) success, result = self.serial_bus.read_bus(tetrad, register_num, device_addr) if success: # result теперь словарь с данными hex_str = ' '.join(f'{b:02X}' for b in result['raw_response']['raw']) self.bus_result_label.setText(f"{hex_str}") # Извлекаем считанное слово read_word = result['value'] self.bus_read_value_label.setText(f"0x{read_word:04X} ({read_word})") self.raw_widget.append_text(f"Чтение из шины: адрес={tetrad}, рег={register_num} = 0x{read_word:04X}") self.last_result = f"Чтение из шины: адрес=0x{tetrad:X}, регистр=0x{register_num:X}" self.last_result += f"\nСчитано: 0x{read_word:04X} ({read_word})" self.last_result += f"\nОтвет контроллера: {hex_str}" else: self.bus_result_label.setText(f"Ошибка: {result}") self.bus_read_value_label.setText("---") self.raw_widget.append_text(f"Чтение из шины: адрес={tetrad}, рег={register_num} - Ошибка: {result}") self.last_result = f"Ошибка чтения из шины: {result}" except ValueError as e: self.bus_result_label.setText(f"Ошибка: {e}") self.bus_read_value_label.setText("---") self.raw_widget.append_text(f"Ошибка в данных: {e}") self.last_result = f"Ошибка в данных: {e}" except ValueError as e: self.bus_result_label.setText(f"Ошибка: {e}") self.bus_read_value_label.setText("---") error_text = f"Ошибка в данных: {e}" self.raw_widget.append_text(f"✗ {error_text}") self.last_result = error_text def create_macro(self): """Создать новый макрос""" dialog = MacroEditorDialog(self) if dialog.exec(): macro = dialog.get_macro() if not macro['name']: macro['name'] = f"Макрос {len(self.macros) + 1}" self.macros.append(macro) self.add_macro_button(macro) self.save_macros() # Сохраняем после добавления dialog.deleteLater() QApplication.processEvents() # Обрабатываем события def add_macro_button_with_index(self, macro, index): """Добавить кнопку макроса в интерфейс с указанным индексом""" items_per_row = 2 row = len(self.macro_buttons) // items_per_row col = len(self.macro_buttons) % items_per_row # Создаем контейнер для кнопки и кнопки редактирования container = QWidget() container_layout = QHBoxLayout() container_layout.setContentsMargins(0, 0, 0, 0) container_layout.setSpacing(4) container.setMaximumWidth(360) container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) # Основная кнопка макроса macro_btn = QPushButton(macro['name']) macro_btn.setFixedHeight(30) macro_btn.setMinimumWidth(120) macro_btn.setToolTip(macro["name"]) macro_btn.setStyleSheet(""" QPushButton { font-weight: bold; text-align: left; padding: 3px 8px; } """) # Используем partial для фиксации индекса macro_btn.clicked.connect(partial(self.execute_macro_by_index, index)) # Кнопка редактирования edit_btn = QPushButton() edit_btn.setIcon(QIcon(_MacroActionIcon("edit", edit_btn))) edit_btn.setIconSize(QSize(16, 16)) edit_btn.setFixedSize(30, 30) edit_btn.setStyleSheet("QPushButton { padding: 0; }") edit_btn.setAccessibleName("Редактировать макрос") edit_btn.setToolTip("Редактировать макрос") edit_btn.clicked.connect(partial(self.edit_macro_by_index, index)) # Кнопка удаления delete_btn = QPushButton() delete_btn.setIcon(QIcon(_MacroActionIcon("delete", delete_btn))) delete_btn.setIconSize(QSize(16, 16)) delete_btn.setFixedSize(30, 30) delete_btn.setStyleSheet("QPushButton { padding: 0; }") delete_btn.setAccessibleName("Удалить макрос") delete_btn.setToolTip("Удалить макрос") delete_btn.clicked.connect(partial(self.delete_macro, index)) container_layout.addWidget(macro_btn) container_layout.addWidget(edit_btn) container_layout.addWidget(delete_btn) container.setLayout(container_layout) self.macros_scroll_layout.addWidget(container, row, col, 1, 1) # span изменен на 1 self.macro_buttons.append(container) def add_macro_button(self, macro): """Добавить кнопку макроса в интерфейс (при создании нового)""" # При создании нового макроса используем последний индекс index = len(self.macros) - 1 self.add_macro_button_with_index(macro, index) def execute_macro_by_index(self, macro_index): """Выполнить макрос по индексу""" if 0 <= macro_index < len(self.macros): self.execute_macro_commands(self.macros[macro_index]['commands']) def edit_macro_by_index(self, macro_index): """Редактировать макрос по индексу""" if 0 <= macro_index < len(self.macros): dialog = MacroEditorDialog(self, self.macros[macro_index]) if dialog.exec(): edited_macro = dialog.get_macro() self.macros[macro_index] = edited_macro # Обновляем кнопку - находим ее по индексу if macro_index < len(self.macro_buttons): container = self.macro_buttons[macro_index] macro_btn = container.layout().itemAt(0).widget() if macro_btn: macro_btn.setText(edited_macro['name']) self.save_macros() dialog.deleteLater() QApplication.processEvents() # Обрабатываем события # Добавим метод для удаления макроса def delete_macro(self, macro_index): """Удалить макрос""" if 0 <= macro_index < len(self.macros): # Спросим подтверждение reply = QMessageBox.question( self, "Удаление макроса", f"Удалить макрос '{self.macros[macro_index]['name']}'?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # Удаляем макрос из списка del self.macros[macro_index] # Полностью перестраиваем layout self.rebuild_macros_layout() # Сохраняем изменения self.save_macros() def execute_macro_commands(self, commands): """Run a macro as Qt callbacks, without loops or waiting in the UI thread.""" if self._macro_running: self.raw_widget.append_text("Дождитесь завершения макроса") return from protocan.tms_terminal import bus_steps try: jobs = [] for command in commands: if command['type'] not in ('write', 'read'): raise ValueError("Неизвестная команда макроса") jobs.extend(bus_steps(command['type'] == 'write', command['tetrad'], command['register'], command.get('data', 0))) except (KeyError, ValueError, RuntimeError, TypeError) as error: self.raw_widget.append_text(f"Макрос остановлен: {error}") return if not jobs: self.raw_widget.append_text("Макрос пуст") return self._macro_jobs = jobs self._macro_index = 0 self._macro_running = True self._macro_generation += 1 self._set_macro_controls(True) generation = self._macro_generation QTimer.singleShot(0, lambda: self._next_macro_command(generation)) def _set_macro_controls(self, running): self.stop_macro_btn.setEnabled(running) for widget in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn, self.macros_scroll_area, self.raw_widget): widget.setEnabled(not running) def _next_macro_command(self, generation): if not self._macro_running or generation != self._macro_generation: return if self._macro_index >= len(self._macro_jobs): self._finish_macro("Макрос выполнен") return generation = self._macro_generation address, value = self._macro_jobs[self._macro_index] self.raw_protocol.request_async(address, value, 10, lambda success, result: self._macro_reply(generation, success, result)) def _macro_reply(self, generation, success, result): if not self._macro_running or generation != self._macro_generation: return if not success: self._finish_macro(f"Макрос остановлен: {result}") return if 'value' in result: value = result['value'] self.bus_read_value_label.setText(f"0x{value:04X} ({value})") self.bus_result_label.setText(result['raw_response']['raw'].hex(" ")) self.raw_widget.append_text(f"Макрос: прочитано 0x{value:04X}") else: self.raw_widget.append_text("Макрос: запись подтверждена") self._macro_index += 1 QTimer.singleShot(0, lambda: self._next_macro_command(generation)) def _finish_macro(self, message): self._macro_running = False self._set_macro_controls(False) self.last_result = message self.raw_widget.append_text(message) def stop_macro(self): if not self._macro_running: return self._macro_generation += 1 self._finish_macro("Макрос остановлен пользователем; переданные команды могли быть выполнены") self.raw_protocol.cancel_async() def load_macros(self): """Загрузить макросы из файла""" if os.path.exists(self.MACROS_FILE): try: with open(self.MACROS_FILE, 'r', encoding='utf-8') as f: self.macros = json.load(f) print(f"Загружено {len(self.macros)} макросов") except Exception as e: print(f"Ошибка загрузки макросов: {e}") self.macros = [] else: self.macros = [] def save_macros(self): """Сохранить макросы в файл""" try: with open(self.MACROS_FILE, 'w', encoding='utf-8') as f: json.dump(self.macros, f, ensure_ascii=False, indent=2) print(f"Сохранено {len(self.macros)} макросов") except Exception as e: print(f"Ошибка сохранения макросов: {e}") def rebuild_macros_layout(self): """Перестроить layout макросов""" # Очищаем layout for i in reversed(range(self.macros_scroll_layout.count())): item = self.macros_scroll_layout.itemAt(i) if item and item.widget(): item.widget().setParent(None) # Очищаем список кнопок self.macro_buttons.clear() # Пересоздаем все кнопки с правильными индексами for i, macro in enumerate(self.macros): self.add_macro_button_with_index(macro, i) def get_last_result(self): """Получить последний результат операции""" result = getattr(self, 'last_result', '') return result