From f089edba8be162a5f31a3e85b3f6050f500f2368 Mon Sep 17 00:00:00 2001 From: Lowa Date: Fri, 14 Feb 2025 23:47:47 +0300 Subject: [PATCH] Refactor settings tab and add dedicated settings window - Create a new `SettingsWindow` class for managing application settings - Simplify the settings tab in the main application - Add first-run detection and settings configuration prompt - Improve settings management with centralized save and load methods - Update menu to include settings option - Enhance user experience with more intuitive settings interface --- ComConfigCopy.py | 322 +++++++++++++++++++++-------------------------- 1 file changed, 146 insertions(+), 176 deletions(-) diff --git a/ComConfigCopy.py b/ComConfigCopy.py index ade7c2c..9d1f710 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - # Это программа для копирования конфигураций на коммутаторы # Программа использует библиотеку PySerial для работы с последовательными портами # Программа использует библиотеку PyQt5 для создания графического интерфейса @@ -411,21 +410,117 @@ def execute_commands_from_file( # Графический интерфейс (Tkinter) # ========================== +class SettingsWindow(tk.Toplevel): + def __init__(self, parent, settings, callback=None): + super().__init__(parent) + self.title("Настройки") + self.geometry("600x400") + self.settings = settings + self.callback = callback + self.resizable(False, False) + + # Создаем фрейм для настроек + settings_frame = ttk.Frame(self, padding="10") + settings_frame.pack(fill=BOTH, expand=True) + + # COM порт + ttk.Label(settings_frame, text="COM порт:").grid(row=0, column=0, sticky=W, pady=5) + self.port_var = StringVar(value=settings.get("port", "")) + self.port_combo = ttk.Combobox(settings_frame, textvariable=self.port_var) + self.port_combo.grid(row=0, column=1, sticky=W, pady=5) + ttk.Button(settings_frame, text="Обновить", command=self.update_ports).grid(row=0, column=2, padx=5) + + # Скорость передачи + ttk.Label(settings_frame, text="Скорость:").grid(row=1, column=0, sticky=W, pady=5) + self.baudrate_var = StringVar(value=str(settings.get("baudrate", 9600))) + baudrate_combo = ttk.Combobox(settings_frame, textvariable=self.baudrate_var, + values=["9600", "19200", "38400", "57600", "115200"]) + baudrate_combo.grid(row=1, column=1, sticky=W, pady=5) + + # Таймаут + ttk.Label(settings_frame, text="Таймаут (сек):").grid(row=2, column=0, sticky=W, pady=5) + self.timeout_var = StringVar(value=str(settings.get("timeout", 10))) + timeout_entry = ttk.Entry(settings_frame, textvariable=self.timeout_var) + timeout_entry.grid(row=2, column=1, sticky=W, pady=5) + + # Режим копирования + ttk.Label(settings_frame, text="Режим копирования:").grid(row=3, column=0, sticky=W, pady=5) + self.copy_mode_var = StringVar(value=settings.get("copy_mode", "line")) + copy_mode_frame = ttk.Frame(settings_frame) + copy_mode_frame.grid(row=3, column=1, sticky=W, pady=5) + ttk.Radiobutton(copy_mode_frame, text="Построчно", value="line", + variable=self.copy_mode_var).pack(side=LEFT) + ttk.Radiobutton(copy_mode_frame, text="Блоками", value="block", + variable=self.copy_mode_var).pack(side=LEFT) + + # Размер блока + ttk.Label(settings_frame, text="Размер блока:").grid(row=4, column=0, sticky=W, pady=5) + self.block_size_var = StringVar(value=str(settings.get("block_size", 15))) + block_size_entry = ttk.Entry(settings_frame, textvariable=self.block_size_var) + block_size_entry.grid(row=4, column=1, sticky=W, pady=5) + + # Приглашение командной строки + ttk.Label(settings_frame, text="Приглашение:").grid(row=5, column=0, sticky=W, pady=5) + self.prompt_var = StringVar(value=settings.get("prompt", ">")) + prompt_entry = ttk.Entry(settings_frame, textvariable=self.prompt_var) + prompt_entry.grid(row=5, column=1, sticky=W, pady=5) + + # Кнопки + button_frame = ttk.Frame(settings_frame) + button_frame.grid(row=6, column=0, columnspan=3, pady=20) + ttk.Button(button_frame, text="Сохранить", command=self.save_settings).pack(side=LEFT, padx=5) + ttk.Button(button_frame, text="Отмена", command=self.destroy).pack(side=LEFT, padx=5) + + self.update_ports() + + # Центрируем окно + self.center_window() + + def center_window(self): + self.update_idletasks() + width = self.winfo_width() + height = self.winfo_height() + x = (self.winfo_screenwidth() // 2) - (width // 2) + y = (self.winfo_screenheight() // 2) - (height // 2) + self.geometry(f"{width}x{height}+{x}+{y}") + + def update_ports(self): + ports = list_serial_ports() + self.port_combo["values"] = ports + if ports and not self.port_var.get(): + self.port_var.set(ports[0]) + + def save_settings(self): + try: + self.settings.update({ + "port": self.port_var.get(), + "baudrate": int(self.baudrate_var.get()), + "timeout": int(self.timeout_var.get()), + "copy_mode": self.copy_mode_var.get(), + "block_size": int(self.block_size_var.get()), + "prompt": self.prompt_var.get() + }) + settings_save(self.settings) + if self.callback: + self.callback() + self.destroy() + messagebox.showinfo("Успех", "Настройки успешно сохранены") + except ValueError as e: + messagebox.showerror("Ошибка", "Проверьте правильность введенных значений") + class SerialAppGUI(tk.Tk): def __init__(self, settings): super().__init__() self.title("Serial Device Manager") self.geometry("900x700") - # Настройка ttk стилей self.style = ttk.Style(self) self.style.theme_use("clam") default_font = ("Segoe UI", 10) self.option_add("*Font", default_font) self.settings = settings self.connection = None - # self.tftp_thread = None # TFTP сервер отключен - # Глобальные биндинги для копирования, вставки и вырезания + # Глобальные биндинги self.bind_class("Text", "", lambda event: event.widget.event_generate("<>")) self.bind_class("Text", "", lambda event: event.widget.event_generate("<>")) self.bind_class("Text", "", lambda event: event.widget.event_generate("<>")) @@ -435,134 +530,59 @@ class SerialAppGUI(tk.Tk): self.create_menu() self.create_tabs() - + + # Проверка первого запуска + self.check_first_run() + + def check_first_run(self): + # Проверяем, существует ли папка Settings и файл settings.json + if not os.path.exists("Settings") or not os.path.exists(SETTINGS_FILE): + # Создаем папку Settings, если её нет + os.makedirs("Settings", exist_ok=True) + + response = messagebox.askyesno( + "Первый запуск", + "Это первый запуск программы. Хотите настроить параметры подключения сейчас?" + ) + if response: + self.open_settings() + def create_menu(self): menubar = tk.Menu(self) self.config(menu=menubar) + + # Меню "Файл" file_menu = tk.Menu(menubar, tearoff=0) - file_menu.add_command(label="Выход", command=self.quit) menubar.add_cascade(label="Файл", menu=file_menu) - + file_menu.add_command(label="Настройки", command=self.open_settings) + file_menu.add_separator() + file_menu.add_command(label="Выход", command=self.quit) + + def open_settings(self): + settings_window = SettingsWindow(self, self.settings, self.on_settings_changed) + settings_window.transient(self) + settings_window.grab_set() + + def on_settings_changed(self): + # Обновляем настройки в основном приложении + self.settings = settings_load() + def create_tabs(self): - notebook = ttk.Notebook(self) - notebook.pack(fill=BOTH, expand=True, padx=5, pady=5) - - # Вкладка "Настройки" - self.settings_frame = ttk.Frame(notebook, padding=10) - notebook.add(self.settings_frame, text="Настройки") - self.create_settings_tab(self.settings_frame) - - # Вкладка "Интерактивный режим" - self.interactive_frame = ttk.Frame(notebook, padding=10) - notebook.add(self.interactive_frame, text="Интерактивный режим") - self.create_interactive_tab(self.interactive_frame) - - # Вкладка "Выполнить команды из файла" - self.file_exec_frame = ttk.Frame(notebook, padding=10) - notebook.add(self.file_exec_frame, text="Выполнить команды из файла") - self.create_file_exec_tab(self.file_exec_frame) - - # Вкладка "Редактор конфигурационного файла" - self.config_editor_frame = ttk.Frame(notebook, padding=10) - notebook.add(self.config_editor_frame, text="Редактор конфигурационного файла") - self.create_config_editor_tab(self.config_editor_frame) - - # # Вкладка "TFTP Сервер" (отключено) - # self.tftp_frame = ttk.Frame(notebook, padding=10) - # notebook.add(self.tftp_frame, text="TFTP Сервер") - # self.create_tftp_tab(self.tftp_frame) - - # -------------- Вкладка "Настройки" -------------- - def create_settings_tab(self, frame): - # Используем grid с отступами - ttk.Label(frame, text="COM-порт:").grid(row=0, column=0, sticky=tk.E, padx=5, pady=5) - self.port_var = StringVar(value=self.settings.get("port") or "") - self.port_combo = ttk.Combobox(frame, textvariable=self.port_var, values=list_serial_ports(), width=20) - self.port_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) - ttk.Button(frame, text="Обновить", command=self.update_ports).grid(row=0, column=2, padx=5, pady=5) - - ttk.Label(frame, text="Baudrate:").grid(row=1, column=0, sticky=tk.E, padx=5, pady=5) - self.baud_var = StringVar(value=str(self.settings.get("baudrate"))) - ttk.Entry(frame, textvariable=self.baud_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=5, pady=5) - - ttk.Label(frame, text="Файл конфигурации:").grid(row=2, column=0, sticky=tk.E, padx=5, pady=5) - self.config_var = StringVar(value=self.settings.get("config_file") or "") - ttk.Entry(frame, textvariable=self.config_var, width=40).grid(row=2, column=1, sticky=tk.W, padx=5, pady=5) - ttk.Button(frame, text="Выбрать", command=self.select_config_file).grid(row=2, column=2, padx=5, pady=5) - - ttk.Label(frame, text="Логин:").grid(row=3, column=0, sticky=tk.E, padx=5, pady=5) - self.login_var = StringVar(value=self.settings.get("login") or "") - ttk.Entry(frame, textvariable=self.login_var, width=20).grid(row=3, column=1, sticky=tk.W, padx=5, pady=5) - - ttk.Label(frame, text="Пароль:").grid(row=4, column=0, sticky=tk.E, padx=5, pady=5) - self.password_var = StringVar(value=self.settings.get("password") or "") - ttk.Entry(frame, textvariable=self.password_var, show="*", width=20).grid(row=4, column=1, sticky=tk.W, padx=5, pady=5) - - ttk.Label(frame, text="Timeout (сек):").grid(row=5, column=0, sticky=tk.E, padx=5, pady=5) - self.timeout_var = StringVar(value=str(self.settings.get("timeout"))) - ttk.Entry(frame, textvariable=self.timeout_var, width=10).grid(row=5, column=1, sticky=tk.W, padx=5, pady=5) - - ttk.Label(frame, text="Режим копирования:").grid(row=6, column=0, sticky=tk.E, padx=5, pady=5) - self.copy_mode_var = StringVar(value=self.settings.get("copy_mode")) - ttk.Radiobutton(frame, text="Построчно", variable=self.copy_mode_var, value="line").grid(row=6, column=1, sticky=tk.W, padx=5) - ttk.Radiobutton(frame, text="Блочно", variable=self.copy_mode_var, value="block").grid(row=6, column=1, padx=100, sticky=tk.W) - - ttk.Label(frame, text="Размер блока (строк):").grid(row=7, column=0, sticky=tk.E, padx=5, pady=5) - self.block_size_var = StringVar(value=str(self.settings.get("block_size"))) - ttk.Entry(frame, textvariable=self.block_size_var, width=10).grid(row=7, column=1, sticky=tk.W, padx=5, pady=5) - - ttk.Label(frame, text="Паттерн приглашения:").grid(row=8, column=0, sticky=tk.E, padx=5, pady=5) - self.prompt_var = StringVar(value=self.settings.get("prompt")) - ttk.Entry(frame, textvariable=self.prompt_var, width=20).grid(row=8, column=1, sticky=tk.W, padx=5, pady=5) - - ttk.Button(frame, text="Сохранить настройки", command=self.save_settings).grid(row=9, column=0, padx=5, pady=10) - ttk.Button(frame, text="Проверить соединение", command=self.check_connection).grid(row=9, column=1, padx=5, pady=10) - - def update_ports(self): - ports = list_serial_ports() - self.port_combo['values'] = ports - messagebox.showinfo("Информация", "Список портов обновлен.") - - def select_config_file(self): - filename = filedialog.askopenfilename(title="Выберите файл конфигурации", filetypes=[("Text files", "*.txt")]) - if filename: - self.config_var.set(filename) - - def save_settings(self): - self.settings["port"] = self.port_var.get() - try: - self.settings["baudrate"] = int(self.baud_var.get()) - except ValueError: - messagebox.showerror("Ошибка", "Некорректное значение baudrate!") - return - self.settings["config_file"] = self.config_var.get() - self.settings["login"] = self.login_var.get() - self.settings["password"] = self.password_var.get() - try: - self.settings["timeout"] = int(self.timeout_var.get()) - except ValueError: - messagebox.showerror("Ошибка", "Некорректное значение timeout!") - return - self.settings["copy_mode"] = self.copy_mode_var.get() - try: - self.settings["block_size"] = int(self.block_size_var.get()) - except ValueError: - messagebox.showerror("Ошибка", "Некорректное значение размера блока!") - return - self.settings["prompt"] = self.prompt_var.get() - settings_save(self.settings) - messagebox.showinfo("Информация", "Настройки сохранены.") - - def check_connection(self): - if not self.settings.get("port"): - messagebox.showerror("Ошибка", "COM-порт не выбран!") - return - conn = create_connection(self.settings) - if conn: - messagebox.showinfo("Информация", "Соединение установлено успешно!") - conn.close() - else: - messagebox.showerror("Ошибка", "Не удалось установить соединение.") + self.notebook = ttk.Notebook(self) + self.notebook.pack(fill=BOTH, expand=True, padx=5, pady=5) + + # Создаем вкладки + interactive_frame = ttk.Frame(self.notebook) + file_exec_frame = ttk.Frame(self.notebook) + config_editor_frame = ttk.Frame(self.notebook) + + self.notebook.add(interactive_frame, text="Интерактивный режим") + self.notebook.add(file_exec_frame, text="Выполнение файла") + self.notebook.add(config_editor_frame, text="Редактор конфигурации") + + self.create_interactive_tab(interactive_frame) + self.create_file_exec_tab(file_exec_frame) + self.create_config_editor_tab(config_editor_frame) # -------------- Вкладка "Интерактивный режим" -------------- def create_interactive_tab(self, frame): @@ -760,56 +780,6 @@ class SerialAppGUI(tk.Tk): logging.error(f"Ошибка сохранения файла: {e}", exc_info=True) messagebox.showerror("Ошибка", f"Не удалось сохранить файл:\n{e}") - # -------------- Вкладка "TFTP Сервер" (отключено) -------------- - # def create_tftp_tab(self, frame): - # ip_frame = ttk.Frame(frame) - # ip_frame.pack(fill=X, pady=2) - # ttk.Label(ip_frame, text="Слушать на IP:").pack(side=LEFT, padx=5) - # self.tftp_listen_ip_var = StringVar(value="0.0.0.0") - # ttk.Entry(ip_frame, textvariable=self.tftp_listen_ip_var, width=15).pack(side=LEFT, padx=5) - # - # port_frame = ttk.Frame(frame) - # port_frame.pack(fill=X, pady=2) - # ttk.Label(port_frame, text="Порт:").pack(side=LEFT, padx=5) - # self.tftp_port_var = StringVar(value="6969") - # ttk.Entry(port_frame, textvariable=self.tftp_port_var, width=10).pack(side=LEFT, padx=5) - # - # folder_frame = ttk.Frame(frame) - # folder_frame.pack(fill=X, pady=2) - # ttk.Label(folder_frame, text="Папка прошивок:").pack(side=LEFT, padx=5) - # self.firmware_folder_var = StringVar(value="Firmware") - # ttk.Label(folder_frame, textvariable=self.firmware_folder_var).pack(side=LEFT, padx=5) - # ttk.Button(folder_frame, text="Открыть папку", command=lambda: os.startfile(os.path.abspath("Firmware"))).pack(side=LEFT, padx=5) - # - # button_frame = ttk.Frame(frame) - # button_frame.pack(fill=X, pady=2) - # ttk.Button(button_frame, text="Запустить TFTP сервер", command=self.start_tftp_server).pack(side=LEFT, padx=5) - # ttk.Button(button_frame, text="Остановить TFTP сервер", command=self.stop_tftp_server).pack(side=LEFT, padx=5) - # - # self.tftp_console = tk.Text(frame, wrap="word", height=15) - # self.tftp_console.pack(fill=BOTH, expand=True, padx=5, pady=5) - # - # def append_tftp_console(self, text): - # self.tftp_console.insert(END, text) - # self.tftp_console.see(END) - - # def start_tftp_server(self): - # listen_ip = self.tftp_listen_ip_var.get() - # try: - # port = int(self.tftp_port_var.get()) - # except ValueError: - # messagebox.showerror("Ошибка", "Некорректное значение порта.") - # return - # self.tftp_thread = TFTPServerThread(listen_ip, port, self.append_tftp_console) - # self.tftp_thread.start() - - # def stop_tftp_server(self): - # if self.tftp_thread: - # self.tftp_thread.stop() - # self.tftp_thread.join() - # self.tftp_thread = None - # self.append_tftp_console("TFTP сервер остановлен.\n") - # ========================== # Парсер аргументов (не используется) # ==========================