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.
337 lines
14 KiB
Python
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,))
|