Files
Py-Backup/pyimage_ui/settings_frame.py
Désiré Werner Menrath a646b9d13a fix(settings): Improve settings logic and fix UI bugs
This commit addresses several bugs and improves the logic of the settings panels.

- fix(settings): The "Reset to default settings" action no longer deletes the user-defined file/folder exclusion list.
- fix(settings): Corrected a bug in the "Add to exclude list" function where new entries were not being written correctly. The logic is now more robust and prevents duplicate entries.
- fix(ui): Fixed a TclError crash in Advanced Settings caused by mixing `grid` and `pack` geometry managers.
- feat(settings): Implemented mutual exclusivity for the "trash bin" and "compression/encryption" checkboxes to prevent invalid configurations.
- i18n: Improved and clarified the English text for the trash bin feature descriptions to enable better translation.
2025-09-09 02:39:16 +02:00

337 lines
14 KiB
Python

import tkinter as tk
from tkinter import ttk
import os
from pathlib import Path
from core.pbp_app_config import AppConfig, Msg
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
from shared_libs.custom_file_dialog import CustomFileDialog
from shared_libs.message import MessageDialog
class SettingsFrame(ttk.Frame):
def __init__(self, master, navigation, actions, **kwargs):
super().__init__(master, **kwargs)
self.navigation = navigation
self.actions = actions
self.pbp_app_config = AppConfig()
self.user_exclude_patterns = []
# --- Container for Treeviews ---
self.trees_container = ttk.Frame(self)
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# --- Treeview for file/folder exclusion ---
self.tree_frame = ttk.LabelFrame(
self.trees_container, text=Msg.STR["user_defined_folder_settings"], padding=10)
self.tree_frame.pack(fill=tk.BOTH, expand=True)
columns = ("included", "name", "path")
self.tree = ttk.Treeview(
self.tree_frame, columns=columns, show="headings")
self.tree.heading("included", text=Msg.STR["in_backup"])
self.tree.heading("name", text=Msg.STR["name"])
self.tree.heading("path", text=Msg.STR["path"])
self.tree.tag_configure("yes", background="#89b4fe")
self.tree.tag_configure("no", background="#4c92ed")
self.tree.column("included", width=100, anchor="center")
self.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.tree.bind("<Button-1>", self._toggle_include_status)
# --- Treeview for hidden files (initially hidden) ---
self.hidden_tree_frame = ttk.LabelFrame(
self.trees_container, text=Msg.STR["hidden_files_and_folders"], padding=10)
self.hidden_tree = ttk.Treeview(
self.hidden_tree_frame, columns=columns, show="headings")
self.hidden_tree.heading("included", text=Msg.STR["in_backup"])
self.hidden_tree.heading("name", text=Msg.STR["name"])
self.hidden_tree.heading("path", text=Msg.STR["path"])
self.hidden_tree.column("included", width=100, anchor="center")
self.hidden_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.hidden_tree.tag_configure("yes", background="#89b4fe")
self.hidden_tree.tag_configure("no", background="#4c92ed")
self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden)
self.hidden_tree_frame.pack_forget() # Initially hidden
# --- Action Buttons ---
self.button_frame = ttk.Frame(self)
self.button_frame.pack(fill=tk.X, padx=10, pady=10)
self.show_hidden_button = ttk.Button(
self.button_frame, command=self._toggle_hidden_files_view, style="TButton.Borderless.Round")
self.show_hidden_button.pack(side=tk.LEFT)
self.unhide_icon = self.master.master.master.image_manager.get_icon(
'hide')
self.hide_icon = self.master.master.master.image_manager.get_icon(
'unhide')
self.show_hidden_button.config(image=self.unhide_icon)
add_to_exclude_button = ttk.Button(
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list)
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
apply_button = ttk.Button(
self.button_frame, text=Msg.STR["apply"], command=self._apply_changes)
apply_button.pack(side=tk.LEFT, padx=5)
cancel_button = ttk.Button(self.button_frame, text=Msg.STR["cancel"],
command=lambda: self.navigation.toggle_mode("backup", 0))
cancel_button.pack(side=tk.LEFT, padx=5)
advanced_button = ttk.Button(
self.button_frame, text=Msg.STR["advanced"], command=self._open_advanced_settings)
advanced_button.pack(side=tk.LEFT, padx=5)
reset_button = ttk.Button(
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings)
reset_button.pack(side=tk.RIGHT)
self.hidden_files_visible = False
# To hold the instance of AdvancedSettingsFrame
self.advanced_settings_frame_instance = None
def _add_to_exclude_list(self) -> bool:
result = MessageDialog("ask", Msg.STR["exclude_dialog_text"], title=Msg.STR["add_to_exclude_list"], buttons=[
Msg.STR["add_folder_button"], Msg.STR["add_file_button"]]).show()
path = None
if result:
dialog = CustomFileDialog(
self, mode="dir", title=Msg.STR["add_to_exclude_list"])
self.wait_window(dialog)
path = dialog.get_result()
dialog.destroy()
else:
dialog = CustomFileDialog(
self, filetypes=[("All Files", "*.*")], title=Msg.STR["add_to_exclude_list"])
self.wait_window(dialog)
path = dialog.get_result()
dialog.destroy()
if path:
try:
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
lines = {line.strip() for line in f if line.strip()}
except FileNotFoundError:
lines = set()
new_entry = f"{path}/*" if os.path.isdir(path) else path
lines.add(new_entry)
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'w') as f:
for line in sorted(list(lines)):
f.write(f"{line}\n")
self.load_and_display_excludes()
if hasattr(self, 'advanced_settings_frame_instance') and self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance._load_manual_excludes()
def show(self):
self.grid(row=2, column=0, sticky="nsew")
self.load_and_display_excludes()
def hide(self):
self.grid_remove()
def _load_exclude_patterns(self):
all_patterns = []
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.GENERATED_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
return all_patterns
def load_and_display_excludes(self):
# Clear existing items
for i in self.tree.get_children():
self.tree.delete(i)
exclude_patterns = self._load_exclude_patterns()
home_dir = Path.home()
items_to_display = []
for item in home_dir.iterdir():
if not item.name.startswith('.'):
item_path_str = str(item.absolute())
is_excluded = f"{item_path_str}/*" in exclude_patterns or item_path_str in exclude_patterns
included_text = Msg.STR["no"] if is_excluded else Msg.STR["yes"]
items_to_display.append(
(included_text, item.name, item_path_str))
# Sort by "Im Backup" status
items_to_display.sort(key=lambda x: x[0], reverse=True)
for item in items_to_display:
tag = "yes" if item[0] == Msg.STR["yes"] else "no"
self.tree.insert("", "end", values=item, tags=(tag,))
def _toggle_include_status(self, event):
item_id = self.tree.identify_row(event.y)
if not item_id:
return
current_values = self.tree.item(item_id, 'values')
current_status = current_values[0]
new_status = Msg.STR["yes"] if current_status == Msg.STR["no"] else Msg.STR["no"]
new_tag = "yes" if new_status == Msg.STR["yes"] else "no"
self.tree.item(item_id, values=(
new_status, current_values[1], current_values[2]), tags=(new_tag,))
def _apply_changes(self):
# Get all paths displayed in the treeviews
tree_paths = set()
for item_id in self.tree.get_children():
values = self.tree.item(item_id, 'values')
tree_paths.add(values[2])
if self.hidden_files_visible:
for item_id in self.hidden_tree.get_children():
values = self.hidden_tree.item(item_id, 'values')
tree_paths.add(values[2])
# Get the new excludes from the treeviews
new_excludes = []
for item_id in self.tree.get_children():
values = self.tree.item(item_id, 'values')
if values[0] == Msg.STR["no"]:
path = values[2]
if os.path.isdir(path):
new_excludes.append(f"{path}/*")
else:
new_excludes.append(path)
if self.hidden_files_visible:
for item_id in self.hidden_tree.get_children():
values = self.hidden_tree.item(item_id, 'values')
if values[0] == Msg.STR["no"]:
path = values[2]
if os.path.isdir(path):
new_excludes.append(f"{path}/*")
else:
new_excludes.append(path)
# Load existing user patterns
existing_user_patterns = []
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
existing_user_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
# Preserve patterns that are not managed by this view
preserved_patterns = []
for pattern in existing_user_patterns:
clean_pattern = pattern.replace('/*', '')
if clean_pattern not in tree_paths:
preserved_patterns.append(pattern)
# Combine preserved patterns with new excludes from this view
final_excludes = list(set(preserved_patterns + new_excludes))
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'w') as f:
for path in final_excludes:
f.write(f"{path}\n")
# Reload the excludes to show the changes
self.load_and_display_excludes()
if self.hidden_files_visible:
self._load_hidden_files()
def _open_advanced_settings(self):
# Hide main settings UI elements
self.trees_container.pack_forget() # Hide the container for treeviews
self.button_frame.pack_forget()
# Create AdvancedSettingsFrame if not already created
if not self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance = AdvancedSettingsFrame(
self, # Parent is now self (SettingsFrame)
config_manager=self.master.master.master.config_manager,
app_instance=self.master.master.master,
show_main_settings_callback=self._show_main_settings
)
# Pack the AdvancedSettingsFrame
self.advanced_settings_frame_instance.pack(fill=tk.BOTH, expand=True)
def _show_main_settings(self):
# Hide advanced settings frame
if self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance.pack_forget()
# Show main settings UI elements
# Re-pack the container for treeviews
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Re-pack the button frame
self.button_frame.pack(fill=tk.X, padx=10, pady=10)
def _toggle_hidden_files_view(self):
self.hidden_files_visible = not self.hidden_files_visible
if self.hidden_files_visible:
self.tree_frame.pack_forget()
self.hidden_tree_frame.pack(fill=tk.BOTH, expand=True)
self._load_hidden_files()
self.show_hidden_button.config(image=self.hide_icon)
else:
self.hidden_tree_frame.pack_forget()
self.tree_frame.pack(fill=tk.BOTH, expand=True)
self.show_hidden_button.config(image=self.unhide_icon)
def _load_hidden_files(self):
# Clear existing items
for i in self.hidden_tree.get_children():
self.hidden_tree.delete(i)
exclude_patterns = self._load_exclude_patterns()
home_dir = Path.home()
items_to_display = []
for item in home_dir.iterdir():
if item.name.startswith('.'):
item_path_str = str(item.absolute())
is_excluded = f"{item_path_str}/*" in exclude_patterns or f"{item_path_str}" in exclude_patterns
included_text = Msg.STR["no"] if is_excluded else Msg.STR["yes"]
items_to_display.append(
(included_text, item.name, item_path_str))
# Sort by name
items_to_display.sort(key=lambda x: x[1])
for item in items_to_display:
tag = "yes" if item[0] == Msg.STR["yes"] else "no"
self.hidden_tree.insert("", "end", values=item, tags=(tag,))
def _toggle_include_status_hidden(self, event):
item_id = self.hidden_tree.identify_row(event.y)
if not item_id:
return
current_values = self.hidden_tree.item(item_id, 'values')
current_status = current_values[0]
new_status = Msg.STR["yes"] if current_status == Msg.STR["no"] else Msg.STR["no"]
new_tag = "yes" if new_status == Msg.STR["yes"] else "no"
self.hidden_tree.item(item_id, values=(
new_status, current_values[1], current_values[2]), tags=(new_tag,))