Improved configuration file management with advanced features

- Added send2trash library for safer file deletion
- Implemented folder and file moving functionality in ConfigSelectorWindow
- Created FolderSelectorDialog for moving files to a folder
- Improved file and folder renaming with improved error handling
- Added emoji icons for better visual representation of actions
- Updated requirements.txt to include send2trash library
- Removed drag-and-drop support. Postponed for future
This commit is contained in:
2025-02-19 23:06:15 +03:00
parent ea432d2893
commit 11253286f8
2 changed files with 425 additions and 263 deletions

View File

@@ -38,6 +38,7 @@ from about_window import AboutWindow
from TFTPServer import TFTPServer from TFTPServer import TFTPServer
import socket import socket
from update_checker import UpdateChecker from update_checker import UpdateChecker
from send2trash import send2trash
# Версия программы # Версия программы
VERSION = "1.0.2" VERSION = "1.0.2"
@@ -1248,201 +1249,201 @@ class SerialAppGUI(tk.Tk):
consecutive_errors = 0 consecutive_errors = 0
MAX_CONSECUTIVE_ERRORS = 3 # Максимальное количество последовательных ошибок MAX_CONSECUTIVE_ERRORS = 3 # Максимальное количество последовательных ошибок
def check_connection():
"""Проверка состояния соединения"""
# Если выполнение остановлено пользователем, просто возвращаем False без сообщений
if self.execution_stop:
return False
if not self.connection or not self.connection.is_open:
# Если остановка произошла во время проверки, не показываем сообщение
if self.execution_stop:
return False
self.append_file_exec_text("[ERROR] Соединение потеряно!\n")
# Автоматически ставим на паузу
self.execution_paused = True
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
# Показываем сообщение только если это не ручная остановка
if not self.execution_stop:
self.after(0, lambda: messagebox.showerror(
"Ошибка соединения",
"Соединение с устройством потеряно!\nВыполнение команд приостановлено.\n\n"
"Пожалуйста:\n"
"1. Проверьте подключение\n"
"2. Нажмите 'Продолжить' после восстановления соединения\n"
" или 'Остановить' для прекращения выполнения"
))
return False
return True
def handle_no_response(cmd_or_block, is_block=False): def handle_no_response(cmd_or_block, is_block=False):
"""Обработка отсутствия ответа от устройства""" """Обработка отсутствия ответа от устройства"""
nonlocal consecutive_errors nonlocal consecutive_errors
consecutive_errors += 1 consecutive_errors += 1
if consecutive_errors >= MAX_CONSECUTIVE_ERRORS: if consecutive_errors >= MAX_CONSECUTIVE_ERRORS:
self.append_file_exec_text( if not self.execution_stop: # Проверяем, не была ли выполнена остановка
f"[ERROR] Обнаружено {consecutive_errors} последовательных ошибок!\n" self.append_file_exec_text(
"Возможно, устройство не отвечает или проблемы с соединением.\n" f"[ERROR] Обнаружено {consecutive_errors} последовательных ошибок!\n"
) "Возможно, устройство не отвечает или проблемы с соединением.\n"
# Автоматически ставим на паузу )
self.execution_paused = True # Автоматически ставим на паузу
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить")) self.execution_paused = True
# Показываем сообщение пользователю self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
self.after(0, lambda: messagebox.showerror( # Показываем сообщение пользователю
"Устройство не отвечает", self.after(0, lambda: messagebox.showerror(
f"Обнаружено {consecutive_errors} последовательных ошибок!\n\n" "Устройство не отвечает",
"Возможные причины:\n" f"Обнаружено {consecutive_errors} последовательных ошибок!\n\n"
"1. Устройство не отвечает на команды\n" "Возможные причины:\n"
"2. Проблемы с соединением\n" "1. Устройство не отвечает на команды\n"
"3. Неверный формат команд\n\n" "2. Проблемы с соединением\n"
"Выполнение приостановлено.\n" "3. Неверный формат команд\n\n"
"Проверьте подключение и состояние устройства,\n" "Выполнение приостановлено.\n"
"затем нажмите 'Продолжить' или 'Остановить'." "Проверьте подключение и состояние устройства,\n"
)) "затем нажмите 'Продолжить' или 'Остановить'."
))
return False return False
return True return True
def wait_before_next_command():
"""Ожидание перед следующей командой с учетом паузы"""
while self.execution_paused and not self.execution_stop:
time.sleep(0.1)
if self.execution_stop:
return False
time.sleep(1) # Базовая задержка между командами
return True
if copy_mode == "line": try:
# Построчный режим if copy_mode == "line":
while self.current_command_index < len(self.commands): # Построчный режим
if self.execution_stop: while self.current_command_index < len(self.commands):
break if self.execution_stop:
break
if self.execution_paused:
time.sleep(0.1)
continue
# Проверяем соединение перед каждой командой
if not check_connection():
continue
cmd = self.commands[self.current_command_index]
try:
success, response = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
# Проверяем соединение после неудачной попытки
if not check_connection():
continue
# Обрабатываем отсутствие ответа
if not handle_no_response(cmd):
continue
else:
# Сбрасываем счетчик ошибок при успешном выполнении
consecutive_errors = 0
self.current_command_index += 1 if not wait_before_next_command():
self.after(0, self.update_progress) break
time.sleep(1) # Задержка между командами
# Проверяем соединение перед каждой командой
except Exception as e: if not self.check_connection():
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении команды: {str(e)}\n") if self.execution_stop: # Если это остановка, прерываем выполнение
if not check_connection(): break
continue continue
break
else: cmd = self.commands[self.current_command_index]
# Блочный режим try:
blocks = generate_command_blocks(self.commands, block_size) success, response = send_command_and_process_response(
total_blocks = len(blocks) self.connection,
current_block = 0 cmd,
self.settings.get("timeout", 10),
while current_block < total_blocks: max_attempts=3,
if self.execution_stop: log_callback=self.append_file_exec_text if not self.execution_stop else None,
break login=self.settings.get("login"),
password=self.settings.get("password"),
if self.execution_paused: is_gui=True
time.sleep(0.1) )
continue
if self.execution_stop:
# Проверяем соединение перед каждым блоком break
if not check_connection():
continue
block = blocks[current_block]
try:
# Выводим блок команд без [CMD] префикса
self.append_file_exec_text(f"Выполнение блока команд:\n{block}\n")
success, response = send_command_and_process_response(
self.connection,
block,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=None, # Отключаем вывод для первой попытки
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success or (response and '^' in response):
self.append_file_exec_text("[WARNING] Ошибка при выполнении блока команд. Отправляю команды по отдельности...\n")
# Проверяем соединение перед отправкой отдельных команд
if not check_connection():
continue
# Обрабатываем отсутствие ответа для блока if not success:
if not success and not handle_no_response(block, True): if not self.execution_stop:
continue self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
# Проверяем соединение после неудачной попытки
# Отправляем команды блока по отдельности if not self.check_connection():
for cmd in block.splitlines(): if self.execution_stop:
if self.execution_stop:
break
if cmd.strip():
if not check_connection():
break break
success, resp = send_command_and_process_response( continue
self.connection, # Обрабатываем отсутствие ответа
cmd, if not handle_no_response(cmd):
self.settings.get("timeout", 10), continue
max_attempts=3, else:
log_callback=self.append_file_exec_text, # Сбрасываем счетчик ошибок при успешном выполнении
login=self.settings.get("login"), consecutive_errors = 0
password=self.settings.get("password"),
is_gui=True self.current_command_index += 1
) self.after(0, self.update_progress)
if not success:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n") except Exception as e:
if not check_connection(): if not self.execution_stop:
break self.append_file_exec_text(f"[ERROR] Ошибка при выполнении команды: {str(e)}\n")
# Обрабатываем отсутствие ответа для отдельной команды break
if not handle_no_response(cmd): else:
break # Блочный режим
else: blocks = generate_command_blocks(self.commands, block_size)
# Сбрасываем счетчик ошибок при успешном выполнении total_blocks = len(blocks)
consecutive_errors = 0 current_block = 0
else:
# Если блок выполнился успешно, выводим ответ и сбрасываем счетчик ошибок while current_block < total_blocks:
consecutive_errors = 0 if self.execution_stop:
if response: break
self.append_file_exec_text(f"Ответ устройства:\n{response}\n")
if not wait_before_next_command():
break
# Обновляем прогресс на основе количества выполненных блоков # Проверяем соединение перед каждым блоком
current_block += 1 if not self.check_connection():
self.current_command_index = (current_block * 100) // total_blocks if self.execution_stop:
self.after(0, self.update_progress) break
time.sleep(1)
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении блока команд: {str(e)}\n")
if not check_connection():
continue continue
break
# Завершение выполнения block = blocks[current_block]
self.after(0, self.execution_completed) try:
if not self.execution_stop:
self.append_file_exec_text(f"Выполнение блока команд:\n{block}\n")
success, response = send_command_and_process_response(
self.connection,
block,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=None,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if self.execution_stop:
break
if not success or (response and '^' in response):
if not self.execution_stop:
self.append_file_exec_text("[WARNING] Ошибка при выполнении блока команд. Отправляю команды по отдельности...\n")
# Проверяем соединение перед отправкой отдельных команд
if not self.check_connection():
if self.execution_stop:
break
continue
# Обрабатываем отсутствие ответа для блока
if not success and not handle_no_response(block, True):
continue
# Отправляем команды блока по отдельности
for cmd in block.splitlines():
if self.execution_stop:
break
if cmd.strip():
if not self.check_connection():
break
success, resp = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text if not self.execution_stop else None,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if self.execution_stop:
break
if not success:
if not self.execution_stop:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
if not self.check_connection():
break
if not handle_no_response(cmd):
break
else:
consecutive_errors = 0
if not wait_before_next_command():
break
# Если блок выполнился успешно
if success and not (response and '^' in response):
consecutive_errors = 0
if response and not self.execution_stop:
self.append_file_exec_text(f"Ответ устройства:\n{response}\n")
# Обновляем прогресс
current_block += 1
self.current_command_index = (current_block * 100) // total_blocks
self.after(0, self.update_progress)
except Exception as e:
if not self.execution_stop:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении блока команд: {str(e)}\n")
break
finally:
# Завершение выполнения
self.after(0, self.execution_completed)
def execution_completed(self): def execution_completed(self):
"""Обработка завершения выполнения в главном потоке""" """Обработка завершения выполнения в главном потоке"""
@@ -1489,27 +1490,38 @@ class SerialAppGUI(tk.Tk):
self.append_file_exec_text("[INFO] Выполнение возобновлено.\n") self.append_file_exec_text("[INFO] Выполнение возобновлено.\n")
def stop_execution(self): def stop_execution(self):
"""Остановка выполнения команд"""
# Устанавливаем флаг остановки
self.execution_stop = True self.execution_stop = True
self.execution_paused = False self.execution_paused = False
self.timer_running = False self.timer_running = False
# Отключаемся от COM-порта # Очищаем очередь команд
if hasattr(self, 'commands'):
delattr(self, 'commands')
# Сбрасываем индекс текущей команды и прогресс
self.current_command_index = 0
self.progress_bar['value'] = 0
self.update_progress()
# Отключаемся от COM-порта без вывода сообщений об ошибках
if self.connection: if self.connection:
try: try:
self.stop_port_monitoring() self.stop_port_monitoring()
self.connection.close() self.connection.close()
except:
pass
finally:
self.connection = None self.connection = None
self.append_file_exec_text("[INFO] Соединение закрыто.\n")
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при закрытии соединения: {str(e)}\n")
self.current_command_index = 0 # Сбрасываем индекс текущей команды # Выводим только одно сообщение об остановке
self.progress_bar['value'] = 0 # Сбрасываем прогресс-бар
self.update_progress() # Обновляем отображение прогресса
self.append_file_exec_text("[INFO] Выполнение остановлено.\n") self.append_file_exec_text("[INFO] Выполнение остановлено.\n")
self.reset_execution_buttons() # Сбрасываем состояние кнопок
self.update_status_bar() # Обновляем статус бар
# Сбрасываем состояние кнопок
self.reset_execution_buttons()
self.update_status_bar()
def reset_execution_buttons(self): def reset_execution_buttons(self):
self.start_button.config(state="normal") self.start_button.config(state="normal")
self.pause_button.config(state="disabled", text="⏸ Пауза") self.pause_button.config(state="disabled", text="⏸ Пауза")
@@ -1959,36 +1971,31 @@ class SerialAppGUI(tk.Tk):
self.baudrate_label.config(text=f"Скорость: {baudrate}") self.baudrate_label.config(text=f"Скорость: {baudrate}")
self.copy_mode_label.config(text=f"Режим: {copy_mode}") self.copy_mode_label.config(text=f"Режим: {copy_mode}")
def on_file_exec_drop(self, event): def check_connection(self):
"""Обработка drop файла во вкладке выполнения команд""" """Проверка состояния соединения"""
try: # Если выполнение остановлено пользователем, просто возвращаем False без сообщений
files = event.data.split() if self.execution_stop:
if files: return False
file = files[0] # Берем только первый файл
if file.lower().endswith('.txt'): if not self.connection or not self.connection.is_open:
filename = os.path.basename(file) # Если это не ручная остановка, показываем сообщение
new_path = os.path.join("Configs", filename) if not self.execution_stop:
self.append_file_exec_text("[ERROR] Соединение потеряно!\n")
if os.path.exists(new_path): # Автоматически ставим на паузу
if not messagebox.askyesno( self.execution_paused = True
"Подтверждение", self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
f"Файл {filename} уже существует. Перезаписать?"
): # Показываем сообщение только если это не ручная остановка
return self.after(0, lambda: messagebox.showerror(
"Ошибка соединения",
# Копируем файл в папку Configs "Соединение с устройством потеряно!\nВыполнение команд приостановлено.\n\n"
with open(file, 'r', encoding='utf-8') as source: "Пожалуйста:\n"
content = source.read() "1. Проверьте подключение\n"
with open(new_path, 'w', encoding='utf-8') as dest: "2. Нажмите 'Продолжить' после восстановления соединения\n"
dest.write(content) " или 'Остановить' для прекращения выполнения"
))
# Устанавливаем путь к файлу return False
self.file_exec_var.set(new_path) return True
messagebox.showinfo("Успех", "Файл конфигурации успешно добавлен")
else:
messagebox.showerror("Ошибка", "Поддерживаются только текстовые файлы (.txt)")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось добавить файл: {str(e)}")
# Класс для окна выбора конфигурации # Класс для окна выбора конфигурации
class ConfigSelectorWindow(tk.Toplevel): class ConfigSelectorWindow(tk.Toplevel):
@@ -2041,12 +2048,20 @@ class ConfigSelectorWindow(tk.Toplevel):
self.tree.column("#0", width=400, stretch=True) self.tree.column("#0", width=400, stretch=True)
self.tree.heading("#0", text="Конфигурация") self.tree.heading("#0", text="Конфигурация")
# Создаем контекстное меню # Создаем контекстное меню для файлов
self.context_menu = tk.Menu(self, tearoff=0) self.file_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label="Редактировать", command=self.edit_selected) self.file_menu.add_command(label="✍️ Редактировать", command=self.edit_selected)
self.context_menu.add_command(label="Переименовать", command=self.rename_selected) self.file_menu.add_command(label="📝 Переименовать", command=self.rename_selected)
self.context_menu.add_separator() self.file_menu.add_command(label="📦 Переместить", command=self.move_selected)
self.context_menu.add_command(label="Удалить", command=self.delete_selected) self.file_menu.add_separator()
self.file_menu.add_command(label="🗑️ Удалить", command=self.delete_selected)
# Создаем контекстное меню для папок
self.folder_menu = tk.Menu(self, tearoff=0)
self.folder_menu.add_command(label="📝 Переименовать", command=self.rename_selected)
self.folder_menu.add_command(label="📦 Переместить", command=self.move_selected)
self.folder_menu.add_separator()
self.folder_menu.add_command(label="🗑️ Удалить", command=self.delete_selected)
# Привязываем события # Привязываем события
self.tree.bind("<Double-1>", self.on_double_click) self.tree.bind("<Double-1>", self.on_double_click)
@@ -2095,8 +2110,13 @@ class ConfigSelectorWindow(tk.Toplevel):
item = self.tree.identify('item', event.x, event.y) item = self.tree.identify('item', event.x, event.y)
if item: if item:
self.tree.selection_set(item) self.tree.selection_set(item)
if self.tree.item(item)['text'].startswith("📄"): # Только для файлов item_text = self.tree.item(item)['text']
self.context_menu.post(event.x_root, event.y_root)
# Определяем, это файл или папка
if item_text.startswith("📁"): # Папка
self.folder_menu.post(event.x_root, event.y_root)
elif item_text.startswith("📄"): # Файл
self.file_menu.post(event.x_root, event.y_root)
def get_selected_item(self): def get_selected_item(self):
"""Получение выбранного элемента""" """Получение выбранного элемента"""
@@ -2113,18 +2133,95 @@ class ConfigSelectorWindow(tk.Toplevel):
self.edit_config(path) self.edit_config(path)
def rename_selected(self): def rename_selected(self):
"""Переименование выбранного файла""" """Переименование выбранного элемента"""
item_id = self.get_selected_item() item_id = self.get_selected_item()
if item_id: if item_id:
path = self.get_full_path(item_id) path = self.get_full_path(item_id)
self.rename_config(item_id, path) item_text = self.tree.item(item_id)['text']
# Определяем, это файл или папка
is_folder = item_text.startswith("📁")
current_name = item_text[2:].strip() # Убираем эмодзи
# Запрашиваем новое имя
new_name = simpledialog.askstring(
"Переименование",
"Введите новое имя:" if is_folder else "Введите новое имя файла:",
initialvalue=current_name
)
if new_name:
try:
if not is_folder and not new_name.endswith('.txt'):
new_name += '.txt'
new_path = os.path.join(os.path.dirname(path), new_name)
if os.path.exists(new_path):
if not messagebox.askyesno(
"Подтверждение",
f"{'Папка' if is_folder else 'Файл'} с таким именем уже существует. Перезаписать?"
):
return
os.rename(path, new_path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переименовать: {str(e)}")
def move_selected(self):
"""Перемещение выбранного элемента"""
item_id = self.get_selected_item()
if item_id:
source_path = self.get_full_path(item_id)
item_text = self.tree.item(item_id)['text']
is_folder = item_text.startswith("📁")
if os.path.exists(source_path):
# Создаем окно выбора папки
folder_selector = FolderSelectorDialog(self, "Configs")
if folder_selector.result:
target_folder = folder_selector.result
try:
# Получаем имя элемента из исходного пути
name = os.path.basename(source_path)
# Формируем путь назначения
target_path = os.path.join(target_folder, name)
# Проверяем, существует ли элемент в целевой папке
if os.path.exists(target_path):
if not messagebox.askyesno(
"Подтверждение",
f"{'Папка' if is_folder else 'Файл'} {name} уже существует в целевой папке. Перезаписать?"
):
return
# Перемещаем элемент
os.rename(source_path, target_path)
self.load_configs() # Обновляем список
messagebox.showinfo("Успех", f"{'Папка' if is_folder else 'Файл'} успешно перемещен(а)")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переместить: {str(e)}")
def delete_selected(self): def delete_selected(self):
"""Удаление выбранного файла""" """Удаление выбранного элемента"""
item_id = self.get_selected_item() item_id = self.get_selected_item()
if item_id: if item_id:
path = self.get_full_path(item_id) path = self.get_full_path(item_id)
self.delete_config(path) item_text = self.tree.item(item_id)['text']
is_folder = item_text.startswith("📁")
if os.path.exists(path):
# Запрашиваем подтверждение
if messagebox.askyesno(
"Подтверждение",
f"Вы действительно хотите переместить {'папку' if is_folder else 'файл'} {os.path.basename(path)} в корзину?"
):
try:
send2trash(path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переместить в корзину: {str(e)}")
def load_configs(self): def load_configs(self):
"""Загрузка конфигураций из папки Configs""" """Загрузка конфигураций из папки Configs"""
@@ -2237,35 +2334,6 @@ class ConfigSelectorWindow(tk.Toplevel):
self.master.load_config_file() self.master.load_config_file()
self.destroy() self.destroy()
def rename_config(self, item_id, old_path):
"""Переименование конфигурации"""
if not os.path.isfile(old_path):
return
# Получаем текущее имя файла без эмодзи
current_name = self.tree.item(item_id)['text']
if current_name.startswith("📄 "):
current_name = current_name[2:].strip()
# Запрашиваем новое имя
new_name = simpledialog.askstring(
"Переименование",
"Введите новое имя файла:",
initialvalue=current_name
)
if new_name:
if not new_name.endswith('.txt'):
new_name += '.txt'
new_path = os.path.join(os.path.dirname(old_path), new_name)
try:
os.rename(old_path, new_path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переименовать файл: {str(e)}")
def delete_config(self, path): def delete_config(self, path):
"""Удаление конфигурации""" """Удаление конфигурации"""
if not os.path.isfile(path): if not os.path.isfile(path):
@@ -2274,13 +2342,13 @@ class ConfigSelectorWindow(tk.Toplevel):
# Запрашиваем подтверждение # Запрашиваем подтверждение
if messagebox.askyesno( if messagebox.askyesno(
"Подтверждение", "Подтверждение",
f"Вы действительно хотите удалить файл {os.path.basename(path)}?" f"Вы действительно хотите переместить файл {os.path.basename(path)} в корзину?"
): ):
try: try:
os.remove(path) send2trash(path)
self.load_configs() # Перезагружаем список self.load_configs() # Перезагружаем список
except Exception as e: except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось удалить файл: {str(e)}") messagebox.showerror("Ошибка", f"Не удалось переместить файл в корзину: {str(e)}")
def on_double_click(self, event): def on_double_click(self, event):
"""Обработка двойного клика""" """Обработка двойного клика"""
@@ -2308,6 +2376,99 @@ class ConfigSelectorWindow(tk.Toplevel):
y = (self.winfo_screenheight() // 2) - (height // 2) y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}") self.geometry(f"{width}x{height}+{x}+{y}")
class FolderSelectorDialog(tk.Toplevel):
def __init__(self, parent, root_folder):
super().__init__(parent)
self.title("Выберите папку")
self.geometry("500x400") # Увеличиваем начальный размер окна
self.result = None
# Создаем основной фрейм с отступами
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=BOTH, expand=True)
# Добавляем метку с инструкцией
ttk.Label(main_frame, text="Выберите папку назначения:").pack(anchor=W, pady=(0, 5))
# Создаем фрейм для дерева и скроллбара
tree_frame = ttk.Frame(main_frame)
tree_frame.pack(fill=BOTH, expand=True)
# Создаем и настраиваем дерево папок
self.tree = ttk.Treeview(tree_frame, selectmode="browse")
self.tree.pack(side=LEFT, fill=BOTH, expand=True)
# Добавляем скроллбар
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
scrollbar.pack(side=RIGHT, fill=Y)
self.tree.configure(yscrollcommand=scrollbar.set)
# Загружаем структуру папок
self.load_folders(root_folder)
# Кнопки
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=X, pady=(10, 0))
# Кнопки справа
ttk.Button(button_frame, text="❌ Отмена", command=self.on_cancel).pack(side=RIGHT, padx=(5, 0))
ttk.Button(button_frame, text="✔️ Выбрать папку", command=self.on_select).pack(side=RIGHT)
# Центрируем окно
self.center_window()
# Делаем окно модальным
self.transient(parent)
self.grab_set()
# Устанавливаем минимальный размер окна
self.minsize(400, 300) # Увеличиваем минимальный размер окна
parent.wait_window(self)
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 load_folders(self, root_folder):
"""Загрузка структуры папок"""
def add_folder(path, parent=""):
try:
items = os.listdir(path)
for item in sorted(items):
full_path = os.path.join(path, item)
if os.path.isdir(full_path):
folder_id = self.tree.insert(
parent,
"end",
text="📁 " + item,
values=(full_path,),
open=False
)
add_folder(full_path, folder_id)
except Exception:
pass
# Добавляем корневую папку
root_id = self.tree.insert("", "end", text="📁 Configs", values=(root_folder,), open=True)
add_folder(root_folder, root_id)
def on_select(self):
"""Обработка выбора папки"""
selection = self.tree.selection()
if selection:
self.result = self.tree.item(selection[0])['values'][0]
self.destroy()
def on_cancel(self):
"""Отмена выбора"""
self.destroy()
# ========================== # ==========================
# Основной запуск приложения # Основной запуск приложения
# ========================== # ==========================

View File

@@ -1,4 +1,5 @@
tftpy>=0.8.0 tftpy>=0.8.0
pyserial>=3.5 pyserial>=3.5
requests>=2.31.0 requests>=2.31.0
packaging>=23.2 packaging>=23.2
send2trash>=1.8.0