refactor(settings): Overhaul Reset and Keyfile UI/UX

This commit introduces a major refactoring of the settings and advanced settings panels to improve user experience and code structure.

Key Changes:

1.  **Reset Functionality Refactored:**
    -   The "Default Settings" logic has been moved from `actions.py` into `settings_frame.py`, where it belongs.
    -   Clicking "Reset" now opens a dedicated view with separate options for a "Default Reset" and a "Hard Reset", each with clear explanations.
    -   A global "Cancel" button was added for this new view, and the layout of the components has been centered and cleaned up.
    -   Error handling for the reset process has been improved to show feedback in the header.

2.  **Keyfile Creation UX Overhauled:**
    -   The "Automation Settings" (Keyfile) view in Advanced Settings is completely redesigned for clarity.
    -   It now explicitly displays the currently selected backup destination, so the user knows which container will be affected.
    -   A detailed description has been added to explain the purpose and prerequisites of creating a keyfile.
    -   The redundant "Apply" button is now hidden in this view.
    -   Feedback for keyfile creation (success or failure) is now shown as a temporary message in the header instead of a blocking dialog.
    -   Error messages are more informative, guiding the user on how to proceed (e.g., by creating an encrypted backup first).

3.  **String Externalization:**
    -   All new UI text for the keyfile and reset features has been added to `pbp_app_config.py` to support translation.
This commit is contained in:
2025-09-15 00:46:47 +02:00
parent 34234d2d14
commit 9406d3f0e2
5 changed files with 179 additions and 119 deletions

View File

@@ -257,7 +257,7 @@ class Msg:
"log": _("Log"), "log": _("Log"),
"full_backup": _("Full backup"), "full_backup": _("Full backup"),
"incremental": _("Incremental"), "incremental": _("Incremental"),
"incremental_backup": _("Incremental backup"), # New "incremental_backup": _("Incremental backup"),
"test_run": _("Test run"), "test_run": _("Test run"),
"start": _("Start"), "start": _("Start"),
"cancel_backup": _("Cancel"), "cancel_backup": _("Cancel"),
@@ -280,8 +280,8 @@ class Msg:
"system_excludes": _("System Excludes"), "system_excludes": _("System Excludes"),
"manual_excludes": _("Manual Excludes"), "manual_excludes": _("Manual Excludes"),
"manual_excludes_info": _("Here, manually add files or folders to be excluded from the backup. Each entry should be on a new line."), "manual_excludes_info": _("Here, manually add files or folders to be excluded from the backup. Each entry should be on a new line."),
"sudoers_info_text": _(f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n" "sudoers_info_text": _(f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
f"with the following content (replace 'user' with the correct username):\n" f"with the following content (replace 'user' with the correct username):\n"
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"), f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
# Menus # Menus
@@ -353,9 +353,9 @@ class Msg:
"header_subtitle": _("Simple GUI for rsync"), "header_subtitle": _("Simple GUI for rsync"),
"encrypted_backup_content": _("Encrypted Backups"), "encrypted_backup_content": _("Encrypted Backups"),
"compressed": _("Compressed"), "compressed": _("Compressed"),
"compression": _("Compression"), # New "compression": _("Compression"),
"encrypted": _("Encrypted"), "encrypted": _("Encrypted"),
"encryption": _("Encryption"), # New "encryption": _("Encryption"),
"bypass_security": _("Bypass security"), "bypass_security": _("Bypass security"),
"refresh_log": _("Refresh log"), "refresh_log": _("Refresh log"),
"comment": _("Kommentar"), "comment": _("Kommentar"),
@@ -370,17 +370,38 @@ class Msg:
"sync_mode_trash_bin": _("Sync Mode: Archive. Files deleted from the source will be moved to a trash folder in the backup."), "sync_mode_trash_bin": _("Sync Mode: Archive. Files deleted from the source will be moved to a trash folder in the backup."),
"sync_mode_no_delete": _("Sync Mode: Additive. Files deleted from the source will be kept in the backup."), "sync_mode_no_delete": _("Sync Mode: Additive. Files deleted from the source will be kept in the backup."),
"encryption_note_system_backup": _("Note: For system backups, encryption only applies to files directly within the /home directory. Folders are not automatically encrypted unless explicitly included in the backup."), "encryption_note_system_backup": _("Note: For system backups, encryption only applies to files directly within the /home directory. Folders are not automatically encrypted unless explicitly included in the backup."),
"keyfile_settings": _("Keyfile Settings"), # New
"backup_defaults_title": _("Backup Defaults"), # New # Advanced Settings - Keyfile
"automation_settings_title": _("Automation Settings"), # New "keyfile_settings": _("Keyfile Settings"),
"create_add_key_file": _("Create/Add Key File"), # New "backup_defaults_title": _("Backup Defaults"),
"key_file_not_created": _("Key file not created."), # New "automation_settings_title": _("Automation Settings"),
"backup_options": _("Backup Options"), # New "create_add_key_file": _("Create/Add Key File"),
"hard_reset": _("Hard reset"), "key_file_not_created": _("Key file not created."),
"keyfile_automation_info": _("A keyfile provides an alternative way to unlock an encrypted backup without entering a password, which is useful for automation.\n\n**Prerequisite:** You must first create at least one encrypted backup for a profile. This process will then add a keyfile to that existing encrypted container."),
"err_no_encrypted_container": _("No encrypted container found at the destination. Please create an encrypted backup for this profile first."),
"enter_existing_password_title": _("Enter Existing Password"),
"keyfile_creation_success": _("Keyfile created successfully!"),
"keyfile_creation_failed": _("Failed to create keyfile. See log for details."),
"keyfile_status_unknown": _("Keyfile status unknown (no destination set)."),
"keyfile_exists": _("Keyfile exists: {key_file_path}"),
"keyfile_not_found_for_dest": _("Keyfile has not been created for this destination."),
"select_profile_first": _("Please select at least one profile from the list."),
"current_destination": _("Current Target Destination"),
"no_destination_selected": _("None. Please select a destination in the main backup view."),
"sudoers_info_text": _(f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
f"with the following content (replace 'user' with the correct username):\n"
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
# General Settings
"backup_options": _("Backup Options"),
"reset": _("Reset"),
"hard_reset_success": _("Hard reset successful.\nRestarting..."), "hard_reset_success": _("Hard reset successful.\nRestarting..."),
"hard_reset_info_manual_restart": _("The application must be restarted as drives are still mounted."), "default_reset_success": _("Default reset successful"),
"default_reset_failed": _("Default reset failed\nPlease click Delete now on Full_delete_config_settings..."),
"default_reset_info": _("Click to restore all settings to their original defaults. Your existing backup schedules and any manually added paths in the exclusion list will be preserved."),
"hard_reset_warning": _("This will reset the application to its initial state, as if it were opened for the first time. This can be useful if you are experiencing problems with the app. Clicking 'Delete now' will delete the '.config/py_backup' folder in your home directory without an additional dialog. The application will then automatically restart."), "hard_reset_warning": _("This will reset the application to its initial state, as if it were opened for the first time. This can be useful if you are experiencing problems with the app. Clicking 'Delete now' will delete the '.config/py_backup' folder in your home directory without an additional dialog. The application will then automatically restart."),
"delete_now": _("Delete now"), "delete_now": _("Delete now"),
"default_config_settings": _("Default config settings"),
"full_delete_config_settings": _("Full delete config settings"), "full_delete_config_settings": _("Full delete config settings"),
"password_required": _("Password Required"), "password_required": _("Password Required"),
"enter_password_prompt": _("Please enter the password for the encrypted backup:"), "enter_password_prompt": _("Please enter the password for the encrypted backup:"),

View File

@@ -456,8 +456,8 @@ class MainApplication(tk.Tk):
self.scheduler_frame.grid_remove() self.scheduler_frame.grid_remove()
def _setup_settings_frame(self): def _setup_settings_frame(self):
self.settings_frame = SettingsFrame( self.settings_frame = SettingsFrame(self.content_frame, self.navigation, self.actions,
self.content_frame, self.navigation, self.actions, self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=(0, 10)) self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=(0, 10))
self.settings_frame.grid(row=2, column=0, sticky="nsew") self.settings_frame.grid(row=2, column=0, sticky="nsew")
self.settings_frame.grid_remove() self.settings_frame.grid_remove()

View File

@@ -77,7 +77,7 @@ class Actions:
self.app.compressed_cb.config(state="normal") self.app.compressed_cb.config(state="normal")
self.app.encrypted_cb.config(state="normal") self.app.encrypted_cb.config(state="normal")
self.app.test_run_cb.config(state="normal") self.app.test_run_cb.config(state="normal")
# Apply mutual exclusion rules for Option A # Apply mutual exclusion rules for Option A
if self.app.compressed_var.get(): if self.app.compressed_var.get():
self.app.incremental_var.set(False) self.app.incremental_var.set(False)
@@ -85,7 +85,7 @@ class Actions:
self.app.incremental_cb.config(state="disabled") self.app.incremental_cb.config(state="disabled")
self.app.encrypted_var.set(False) self.app.encrypted_var.set(False)
self.app.encrypted_cb.config(state="disabled") self.app.encrypted_cb.config(state="disabled")
if self.app.incremental_var.get() or self.app.encrypted_var.get(): if self.app.incremental_var.get() or self.app.encrypted_var.get():
self.app.compressed_var.set(False) self.app.compressed_var.set(False)
self.app.compressed_cb.config(state="disabled") self.app.compressed_cb.config(state="disabled")
@@ -397,47 +397,6 @@ class Actions:
MessageDialog(message_type="error", MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show() title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show()
def reset_to_default_settings(self):
self.app.config_manager.set_setting("backup_destination_path", None)
self.app.config_manager.set_setting("restore_source_path", None)
self.app.config_manager.set_setting("restore_destination_path", None)
self.app.config_manager.remove_setting("backup_animation_type")
self.app.config_manager.remove_setting("calculation_animation_type")
self.app.config_manager.remove_setting("force_full_backup")
self.app.config_manager.remove_setting("force_incremental_backup")
self.app.config_manager.remove_setting("force_compression")
self.app.config_manager.remove_setting("force_encryption")
self.app.update_backup_options_from_config()
AppConfig.generate_and_write_final_exclude_list()
app_logger.log("Settings have been reset to default values.")
settings_frame = self.app.settings_frame
if settings_frame:
settings_frame.load_and_display_excludes()
settings_frame._load_hidden_files()
self.app.destination_path = None
self.app.start_cancel_button.config(state="disabled")
self.app.backup_left_canvas_data.clear()
self.app.backup_right_canvas_data.clear()
self.app.restore_left_canvas_data.clear()
self.app.restore_right_canvas_data.clear()
current_source = self.app.left_canvas_data.get('folder')
if current_source:
self.on_sidebar_button_click(current_source)
self.app.backup_content_frame.system_backups_frame._load_backup_content()
self.app.backup_content_frame.user_backups_frame._load_backup_content()
with message_box_animation(self.app.animated_icon):
MessageDialog(message_type="info",
title=Msg.STR["settings_reset_title"], text=Msg.STR["settings_reset_text"])
def _parse_size_string_to_bytes(self, size_str: str) -> int: def _parse_size_string_to_bytes(self, size_str: str) -> int:
if not size_str or size_str == Msg.STR["calculating_size"]: if not size_str or size_str == Msg.STR["calculating_size"]:
return 0 return 0
@@ -524,7 +483,8 @@ class Actions:
self.app.task_progress.stop() self.app.task_progress.stop()
self.app.task_progress.config(mode="determinate", value=0) self.app.task_progress.config(mode="determinate", value=0)
self.app._update_info_label(Msg.STR["incremental_calc_cancelled"], color="#E8740C") self.app._update_info_label(
Msg.STR["incremental_calc_cancelled"], color="#E8740C")
self.app.start_cancel_button.config(text=Msg.STR["start"]) self.app.start_cancel_button.config(text=Msg.STR["start"])
self._set_ui_state(True) self._set_ui_state(True)
return return
@@ -541,12 +501,14 @@ class Actions:
delete_path) delete_path)
app_logger.log( app_logger.log(
Msg.STR["backup_cancelled_and_deleted_msg"]) Msg.STR["backup_cancelled_and_deleted_msg"])
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"]) self.app._update_info_label(
Msg.STR["backup_cancelled_and_deleted_msg"])
else: else:
self.app.backup_manager.cancel_backup() self.app.backup_manager.cancel_backup()
app_logger.log( app_logger.log(
"Backup cancelled, but directory could not be deleted (path unknown).") "Backup cancelled, but directory could not be deleted (path unknown).")
self.app._update_info_label("Backup cancelled, but directory could not be deleted (path unknown).") self.app._update_info_label(
"Backup cancelled, but directory could not be deleted (path unknown).")
else: else:
self.app.backup_manager.cancel_backup() self.app.backup_manager.cancel_backup()
if delete_path: if delete_path:
@@ -557,14 +519,17 @@ class Actions:
shutil.rmtree(delete_path) shutil.rmtree(delete_path)
app_logger.log( app_logger.log(
Msg.STR["backup_cancelled_and_deleted_msg"]) Msg.STR["backup_cancelled_and_deleted_msg"])
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"]) self.app._update_info_label(
Msg.STR["backup_cancelled_and_deleted_msg"])
except Exception as e: except Exception as e:
app_logger.log(f"Error deleting backup directory: {e}") app_logger.log(f"Error deleting backup directory: {e}")
self.app._update_info_label(f"Error deleting backup directory: {e}") self.app._update_info_label(
f"Error deleting backup directory: {e}")
else: else:
app_logger.log( app_logger.log(
"Backup cancelled, but no path found to delete.") "Backup cancelled, but no path found to delete.")
self.app._update_info_label("Backup cancelled, but no path found to delete.") self.app._update_info_label(
"Backup cancelled, but no path found to delete.")
if hasattr(self.app, 'current_backup_path'): if hasattr(self.app, 'current_backup_path'):
self.app.current_backup_path = None self.app.current_backup_path = None

View File

@@ -166,28 +166,44 @@ class AdvancedSettingsFrame(ttk.Frame):
self.backup_defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left") self.backup_defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
encryption_note.pack(anchor=tk.W, pady=5) encryption_note.pack(anchor=tk.W, pady=5)
# --- Keyfile Settings Frame ---
self.keyfile_settings_frame = ttk.LabelFrame( self.keyfile_settings_frame = ttk.LabelFrame(
view_container, text=Msg.STR["automation_settings_title"], padding=10) view_container, text=Msg.STR["automation_settings_title"], padding=10)
keyfile_info_label = ttk.Label(
self.keyfile_settings_frame, text=Msg.STR["keyfile_automation_info"], justify="left", wraplength=750)
keyfile_info_label.grid(
row=0, column=0, columnspan=3, sticky="w", padx=5, pady=(5, 15))
# Display for current destination
dest_frame = ttk.Frame(self.keyfile_settings_frame)
dest_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
dest_frame.columnconfigure(1, weight=1)
ttk.Label(dest_frame, text=f"{Msg.STR['current_destination']}:", font=("Ubuntu", 11, "bold")).grid(row=0, column=0, sticky="w")
self.keyfile_dest_path_label = ttk.Label(dest_frame, text=Msg.STR["no_destination_selected"], foreground="gray")
self.keyfile_dest_path_label.grid(row=0, column=1, sticky="w", padx=5)
# Action Button
key_file_button = ttk.Button( key_file_button = ttk.Button(
self.keyfile_settings_frame, text=Msg.STR["create_add_key_file"], command=self._create_key_file) self.keyfile_settings_frame, text=Msg.STR["create_add_key_file"], command=self._create_key_file)
key_file_button.grid(row=0, column=0, padx=5, pady=5) key_file_button.grid(row=2, column=0, padx=5, pady=10)
self.key_file_status_var = tk.StringVar( # Status Label
value=Msg.STR["key_file_not_created"]) self.key_file_status_var = tk.StringVar(value="")
key_file_status_label = ttk.Label( key_file_status_label = ttk.Label(
self.keyfile_settings_frame, textvariable=self.key_file_status_var, foreground="gray") self.keyfile_settings_frame, textvariable=self.key_file_status_var, foreground="gray")
key_file_status_label.grid(row=0, column=1, padx=5, pady=5, sticky="w") key_file_status_label.grid(
row=2, column=1, padx=5, pady=10, sticky="w")
sudoers_info_text = Msg.STR["sudoers_info_text"] # Sudoers Info
sudoers_info_label = ttk.Label( sudoers_info_label = ttk.Label(
self.keyfile_settings_frame, text=sudoers_info_text, justify="left") self.keyfile_settings_frame, text=Msg.STR["sudoers_info_text"], justify="left")
sudoers_info_label.grid( sudoers_info_label.grid(
row=1, column=0, columnspan=2, sticky="w", padx=5, pady=5) row=3, column=0, columnspan=3, sticky="w", padx=5, pady=10)
self.keyfile_settings_frame.columnconfigure(1, weight=1) self.keyfile_settings_frame.columnconfigure(1, weight=1)
# --- Bottom Buttons (for most views) ---
self.bottom_button_frame = ttk.Frame(self) self.bottom_button_frame = ttk.Frame(self)
self.bottom_button_frame.pack(pady=10) self.bottom_button_frame.pack(pady=10)
@@ -244,60 +260,57 @@ class AdvancedSettingsFrame(ttk.Frame):
self.config_manager.set_setting("no_trash_bin", no_trash) self.config_manager.set_setting("no_trash_bin", no_trash)
def _create_key_file(self): def _create_key_file(self):
if not self.app_instance.destination_path: header = self.app_instance.header_frame
MessageDialog(message_type="error", title="Error", destination = self.app_instance.destination_path
text="Please select a backup destination first.")
if not destination:
MessageDialog(message_type="error", title=Msg.STR["error"],
text=Msg.STR["select_destination_first"]).show()
return return
pybackup_dir = os.path.join( pybackup_dir = os.path.join(destination, "pybackup")
self.app_instance.destination_path, "pybackup")
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks") container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
if not os.path.exists(container_path): if not os.path.exists(container_path):
MessageDialog(message_type="error", title="Error", MessageDialog(message_type="error", title=Msg.STR["error"],
text="No encrypted container found at the destination.") text=Msg.STR["err_no_encrypted_container"]).show()
return return
password_dialog = PasswordDialog( password_dialog = PasswordDialog(
self, title="Enter Existing Password", confirm=False, translations=Msg.STR) self, title=Msg.STR["enter_existing_password_title"], confirm=False, translations=Msg.STR)
password, _ = password_dialog.get_password() password, _ = password_dialog.get_password()
if not password: if not password:
return return
key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file( key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file(
self.app_instance.destination_path, password) destination, password)
if key_file_path: if key_file_path:
MessageDialog(message_type="info", title="Success", header.show_temporary_message(Msg.STR["keyfile_creation_success"])
text=f"Key file created and added successfully!\nPath: {key_file_path}")
else: else:
MessageDialog(message_type="error", title="Error", header.show_temporary_message(Msg.STR["keyfile_creation_failed"])
text="Failed to create or add key file. See log for details.")
self._update_key_file_status() self._update_key_file_status()
def _update_key_file_status(self): def _update_key_file_status(self):
if not self.app_instance.destination_path: destination = self.app_instance.destination_path
self.key_file_status_var.set( if not destination:
"Key file status unknown (no destination set).") self.keyfile_dest_path_label.config(text=Msg.STR["no_destination_selected"], foreground="gray")
self.key_file_status_var.set(Msg.STR["keyfile_status_unknown"])
return return
# Determine the profile_name based on the current source folder self.keyfile_dest_path_label.config(text=destination, foreground="#FFFFFF") # Adapt color to theme
source_name = self.app_instance.left_canvas_data.get('folder')
if not source_name: source_name = self.app_instance.left_canvas_data.get('folder') or "system"
# Fallback or handle case where no source is selected, perhaps default to "system" or log an error profile_name = "system" if source_name == "Computer" else source_name
# For now, let's assume "system" if no specific folder is selected for keyfile status
profile_name = "system"
else:
profile_name = "system" if source_name == "Computer" else source_name
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path( key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
self.app_instance.destination_path, profile_name) destination, profile_name)
if os.path.exists(key_file_path): if os.path.exists(key_file_path):
self.key_file_status_var.set(f"Key file exists: {key_file_path}") self.key_file_status_var.set(Msg.STR["keyfile_exists"].format(key_file_path=key_file_path))
else: else:
self.key_file_status_var.set( self.key_file_status_var.set(Msg.STR["keyfile_not_found_for_dest"])
"Key file has not been created for this destination.")
def _switch_view(self, index): def _switch_view(self, index):
self.current_view_index = index self.current_view_index = index
@@ -310,7 +323,7 @@ class AdvancedSettingsFrame(ttk.Frame):
self.backup_defaults_frame.pack_forget() self.backup_defaults_frame.pack_forget()
# Show/hide the main action buttons based on the view # Show/hide the main action buttons based on the view
if index == 1: # Manual Excludes view if index in [1, 2]: # Manual Excludes and Keyfile Settings views
self.bottom_button_frame.pack_forget() self.bottom_button_frame.pack_forget()
else: else:
self.bottom_button_frame.pack(pady=10) self.bottom_button_frame.pack(pady=10)

View File

@@ -6,7 +6,6 @@ import sys
from pathlib import Path from pathlib import Path
from core.pbp_app_config import AppConfig, Msg from core.pbp_app_config import AppConfig, Msg
from core.backup_manager import BackupManager
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
from shared_libs.custom_file_dialog import CustomFileDialog from shared_libs.custom_file_dialog import CustomFileDialog
from shared_libs.message import MessageDialog, PasswordDialog from shared_libs.message import MessageDialog, PasswordDialog
@@ -47,19 +46,12 @@ class SettingsFrame(ttk.Frame):
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list, style="Gray.Toolbutton") self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list, style="Gray.Toolbutton")
add_to_exclude_button.pack(side=tk.LEFT, padx=5) add_to_exclude_button.pack(side=tk.LEFT, padx=5)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
reset_button = ttk.Button(
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings, style="Gray.Toolbutton")
reset_button.pack(side=tk.LEFT)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack( ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5) side=tk.LEFT, ipady=15, padx=5)
# Right-aligned buttons # Right-aligned buttons
hard_reset_button = ttk.Button( hard_reset_button = ttk.Button(
self.button_frame, text=Msg.STR["hard_reset"], command=self._toggle_hard_reset_view, style="Gray.Toolbutton") self.button_frame, text=Msg.STR["reset"], command=self._toggle_hard_reset_view, style="Gray.Toolbutton")
hard_reset_button.pack(side=tk.LEFT, padx=5) hard_reset_button.pack(side=tk.LEFT, padx=5)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack( ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
@@ -119,30 +111,93 @@ class SettingsFrame(ttk.Frame):
self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden) self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden)
self.hidden_tree_frame.pack_forget() # Initially hidden self.hidden_tree_frame.pack_forget() # Initially hidden
# --- Default Reset Frame (initially hidden) ---
self.default_reset_frame = ttk.LabelFrame(
self, text=Msg.STR["default_config_settings"], padding=10)
default_reset_label = ttk.Label(
self.default_reset_frame, text=Msg.STR["default_reset_info"], wraplength=400, justify=tk.CENTER)
default_reset_label.pack(pady=10, expand=True)
# Frame to center the button
default_reset_button_frame = ttk.Frame(self.default_reset_frame)
default_reset_button_frame.pack(pady=5)
reset_button = ttk.Button(
default_reset_button_frame, text=Msg.STR["default_settings"], command=self._reset_to_default_settings)
reset_button.pack() # Centered by default in its own frame
# --- Hard Reset Frame (initially hidden) --- # --- Hard Reset Frame (initially hidden) ---
self.hard_reset_frame = ttk.LabelFrame( self.hard_reset_frame = ttk.LabelFrame(
self, text=Msg.STR["full_delete_config_settings"], padding=10) self, text=Msg.STR["full_delete_config_settings"], padding=10)
hard_reset_label = ttk.Label( hard_reset_label = ttk.Label(
self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.LEFT) self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.CENTER)
hard_reset_label.pack(pady=10) hard_reset_label.pack(pady=10, expand=True)
hard_reset_button_frame = ttk.Frame(self.hard_reset_frame) hard_reset_button_frame = ttk.Frame(self.hard_reset_frame)
hard_reset_button_frame.pack(pady=10) hard_reset_button_frame.pack(pady=5)
self.delete_now_button = ttk.Button( self.delete_now_button = ttk.Button(
hard_reset_button_frame, text=Msg.STR["delete_now"], command=self._perform_hard_reset) hard_reset_button_frame, text=Msg.STR["delete_now"], command=self._perform_hard_reset)
self.delete_now_button.pack(side=tk.LEFT, padx=5) self.delete_now_button.pack() # Centered by default
self.cancel_hard_reset_button = ttk.Button( # --- Bottom Cancel Button for Reset View (initially hidden) ---
hard_reset_button_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view) self.reset_view_cancel_frame = ttk.Frame(self)
self.cancel_hard_reset_button.pack(side=tk.LEFT, padx=5) self.reset_view_cancel_button = ttk.Button(
self.reset_view_cancel_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view)
self.reset_view_cancel_button.pack(pady=10)
self.hidden_files_visible = False self.hidden_files_visible = False
self.hard_reset_visible = False self.hard_reset_visible = False
# To hold the instance of AdvancedSettingsFrame # To hold the instance of AdvancedSettingsFrame
self.advanced_settings_frame_instance = None self.advanced_settings_frame_instance = None
def _reset_to_default_settings(self):
app_instance = self.master.master.master
header = app_instance.header_frame
try:
self.config_manager.set_setting("backup_destination_path", None)
self.config_manager.set_setting("restore_source_path", None)
self.config_manager.set_setting("restore_destination_path", None)
self.config_manager.remove_setting("backup_animation_type")
self.config_manager.remove_setting("calculation_animation_type")
self.config_manager.remove_setting("force_full_backup")
self.config_manager.remove_setting("force_incremental_backup")
self.config_manager.remove_setting("force_compression")
self.config_manager.remove_setting("force_encryption")
app_instance.update_backup_options_from_config()
AppConfig.generate_and_write_final_exclude_list()
app_logger.log("Settings have been reset to default values.")
self.load_and_display_excludes()
if self.hidden_files_visible:
self._load_hidden_files()
app_instance.destination_path = None
app_instance.start_cancel_button.config(state="disabled")
app_instance.backup_left_canvas_data.clear()
app_instance.backup_right_canvas_data.clear()
app_instance.restore_left_canvas_data.clear()
app_instance.restore_right_canvas_data.clear()
current_source = app_instance.left_canvas_data.get('folder')
if current_source:
self.actions.on_sidebar_button_click(current_source)
app_instance.backup_content_frame.system_backups_frame._load_backup_content()
app_instance.backup_content_frame.user_backups_frame._load_backup_content()
header.show_temporary_message(Msg.STR["default_reset_success"])
except Exception as e:
app_logger.log(f"Error resetting settings: {e}")
header.show_temporary_message(Msg.STR["default_reset_failed"])
def _perform_hard_reset(self): def _perform_hard_reset(self):
try: try:
# First, always attempt to unmount all encrypted drives # First, always attempt to unmount all encrypted drives
@@ -153,7 +208,7 @@ class SettingsFrame(ttk.Frame):
app_logger.log( app_logger.log(
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.") "WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
MessageDialog( MessageDialog(
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"]).show() message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"])
return # Prevent application from closing return # Prevent application from closing
# Try to delete the config directory without elevated rights # Try to delete the config directory without elevated rights
@@ -179,10 +234,16 @@ class SettingsFrame(ttk.Frame):
self.trees_container.pack_forget() self.trees_container.pack_forget()
self.button_frame.pack_forget() self.button_frame.pack_forget()
self.bottom_button_frame.pack_forget() self.bottom_button_frame.pack_forget()
self.default_reset_frame.pack(
fill=tk.X, expand=True, padx=10, pady=5)
self.hard_reset_frame.pack( self.hard_reset_frame.pack(
fill=tk.BOTH, expand=True, padx=10, pady=5) fill=tk.X, expand=True, padx=10, pady=5)
self.reset_view_cancel_frame.pack(fill=tk.X, side=tk.BOTTOM)
else: else:
self.default_reset_frame.pack_forget()
self.hard_reset_frame.pack_forget() self.hard_reset_frame.pack_forget()
self.reset_view_cancel_frame.pack_forget()
self.button_frame.pack(fill=tk.X, padx=10) self.button_frame.pack(fill=tk.X, padx=10)
self.trees_container.pack( self.trees_container.pack(
fill=tk.BOTH, expand=True, padx=10, pady=5) fill=tk.BOTH, expand=True, padx=10, pady=5)