From b6a0bb82f1524871f2b52eafc249a1e4d255572e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Wed, 10 Sep 2025 01:04:10 +0200 Subject: [PATCH] feat(ui): Ersetze Checkboxen und Radio-Buttons durch Switches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dieses Commit ersetzt die meisten ttk.Checkbutton- und ttk.Radiobutton-Widgets in der gesamten Anwendung durch einen benutzerdefinierten "Switch"-Stil, um ein moderneres Erscheinungsbild zu erzielen. Die Änderungen umfassen: - **Hauptfenster**: - Umwandlung der Backup-Optionen (Voll, Inkrementell, Komprimiert, Verschlüsselt) in Switches. - Ersetzung der "Genaue Grössenberechnung"-Checkbox durch einen normalen Button. - Verschiebung der "Testlauf"- und "Sicherheit umgehen"-Switches in die Seitenleiste unter "Einstellungen". - **Scheduler**: - Ersetzung aller Radio-Buttons und Checkboxen durch Switches, mit implementierter Logik zur Gewährleistung der exklusiven Auswahl für Backup-Typ und Frequenz. - **Erweiterte Einstellungen**: - Umwandlung aller Checkboxen im Abschnitt "Backup-Standards" in Switches. - **Styling**: - Hinzufügen eines neuen Stils `Switch2.TCheckbutton` für die Switches in der Seitenleiste, um sie an das dunkle Thema der Seitenleiste anzupassen. Die Konfiguration erfolgt direkt in `main_app.py`. - **Fehlerbehebungen**: - Behebung eines `AttributeError`-Absturzes, der durch die Verschiebung der Switches vor der Deklaration ihrer `tk.BooleanVar`-Variablen verursacht wurde. - Anpassung der zugehörigen Logik in `pyimage_ui/actions.py` an den neuen Button. --- core/backup_manager.py | 4 +- main_app.py | 51 +++++++++++---------- pyimage_ui/actions.py | 21 ++------- pyimage_ui/advanced_settings_frame.py | 14 +++--- pyimage_ui/navigation.py | 2 - pyimage_ui/scheduler_frame.py | 66 ++++++++++++++++++++------- 6 files changed, 91 insertions(+), 67 deletions(-) diff --git a/core/backup_manager.py b/core/backup_manager.py index 2903ada..71c0fc3 100644 --- a/core/backup_manager.py +++ b/core/backup_manager.py @@ -286,8 +286,10 @@ class BackupManager: self.logger.log( f"Executing rsync dry-run command: {' '.join(command)}") + env = os.environ.copy() + env['LC_ALL'] = 'C' result = subprocess.run( - command, capture_output=True, text=True, check=False) + command, capture_output=True, text=True, check=False, env=env) # rsync exit code 24 means some files vanished during transfer, which is okay for a dry-run estimate. if result.returncode != 0 and result.returncode != 24: diff --git a/main_app.py b/main_app.py index 34f69a4..b6e9a7a 100644 --- a/main_app.py +++ b/main_app.py @@ -52,6 +52,11 @@ class MainApplication(tk.Tk): self.style.configure("Green.Sidebar.TButton", foreground="green") + self.style.configure("Switch2.TCheckbutton", background="#2b3e4f", foreground="white") + self.style.map("Switch2.TCheckbutton", + background=[("active", "#2b3e4f"), ("selected", "#2b3e4f"), ("disabled", "#2b3e4f")], + foreground=[("active", "white"), ("selected", "white"), ("disabled", "#737373")]) + main_frame = ttk.Frame(self) main_frame.grid(row=0, column=0, sticky="nsew") self.grid_rowconfigure(0, weight=1) @@ -88,6 +93,14 @@ class MainApplication(tk.Tk): self.navigation = Navigation(self) self.actions = Actions(self) + self.vollbackup_var = tk.BooleanVar() + self.inkrementell_var = tk.BooleanVar() + self.genaue_berechnung_var = tk.BooleanVar() + self.testlauf_var = tk.BooleanVar() + self.compressed_var = tk.BooleanVar() + self.encrypted_var = tk.BooleanVar() + self.bypass_security_var = tk.BooleanVar() + self.mode = "backup" # Default mode self.backup_is_running = False self.start_time = None @@ -154,6 +167,13 @@ class MainApplication(tk.Tk): self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton") self.settings_button.pack(fill=tk.X, pady=10) + self.test_run_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["test_run"], + variable=self.testlauf_var, style="Switch2.TCheckbutton") + self.test_run_cb.pack(fill=tk.X, pady=10) + self.bypass_security_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["bypass_security"], + variable=self.bypass_security_var, style="Switch2.TCheckbutton") + self.bypass_security_cb.pack(fill=tk.X, pady=10) + self.header_frame = HeaderFrame( self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self) self.header_frame.grid(row=0, column=0, sticky="nsew") @@ -305,7 +325,6 @@ class MainApplication(tk.Tk): self.restore_size_frame_after.grid_remove() self._load_state_and_initialize() - self.update_backup_options_from_config() self.protocol("WM_DELETE_WINDOW", self.on_closing) def _load_state_and_initialize(self): @@ -421,14 +440,6 @@ class MainApplication(tk.Tk): self.backup_content_frame.grid_remove() def _setup_task_bar(self): - self.vollbackup_var = tk.BooleanVar() - self.inkrementell_var = tk.BooleanVar() - self.genaue_berechnung_var = tk.BooleanVar() - self.testlauf_var = tk.BooleanVar() - self.compressed_var = tk.BooleanVar() - self.encrypted_var = tk.BooleanVar() - self.bypass_security_var = tk.BooleanVar() - self.info_checkbox_frame = ttk.Frame(self.content_frame, padding=10) self.info_checkbox_frame.grid(row=3, column=0, sticky="ew") @@ -458,9 +469,9 @@ class MainApplication(tk.Tk): accurate_size_frame = ttk.Frame(self.time_info_frame) accurate_size_frame.pack(side=tk.LEFT, padx=20) - self.accurate_size_cb = ttk.Checkbutton(accurate_size_frame, text=Msg.STR["accurate_size_cb_label"], - variable=self.genaue_berechnung_var, command=self.actions.on_toggle_accurate_size_calc) - self.accurate_size_cb.pack(side=tk.LEFT, padx=5) + self.accurate_size_btn = ttk.Button(accurate_size_frame, text=Msg.STR["accurate_size_cb_label"], + command=self.actions.on_accurate_size_calc) + self.accurate_size_btn.pack(side=tk.LEFT, padx=5) accurate_size_info_label = ttk.Label( accurate_size_frame, text=Msg.STR["accurate_size_info_label"], foreground="gray") @@ -470,24 +481,18 @@ class MainApplication(tk.Tk): checkbox_frame.pack(fill=tk.X, pady=5) self.full_backup_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["full_backup"], - variable=self.vollbackup_var, command=lambda: self.actions.handle_backup_type_change('voll')) + variable=self.vollbackup_var, command=lambda: self.actions.handle_backup_type_change('voll'), style="Switch.TCheckbutton") self.full_backup_cb.pack(side=tk.LEFT, padx=5) self.incremental_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["incremental"], - variable=self.inkrementell_var, command=lambda: self.actions.handle_backup_type_change('inkrementell')) + variable=self.inkrementell_var, command=lambda: self.actions.handle_backup_type_change('inkrementell'), style="Switch.TCheckbutton") self.incremental_cb.pack(side=tk.LEFT, padx=5) self.compressed_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["compressed"], - variable=self.compressed_var, command=self.actions.handle_compression_change) + variable=self.compressed_var, command=self.actions.handle_compression_change, style="Switch.TCheckbutton") self.compressed_cb.pack(side=tk.LEFT, padx=5) self.encrypted_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["encrypted"], - variable=self.encrypted_var, command=self.actions.handle_encryption_change) + variable=self.encrypted_var, command=self.actions.handle_encryption_change, style="Switch.TCheckbutton") self.encrypted_cb.pack(side=tk.LEFT, padx=5) - self.test_run_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["test_run"], - variable=self.testlauf_var) - self.test_run_cb.pack(side=tk.LEFT, padx=5) - self.bypass_security_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["bypass_security"], - variable=self.bypass_security_var) - self.bypass_security_cb.pack(side=tk.LEFT, padx=5) self.action_frame = ttk.Frame(self.content_frame, padding=10) self.action_frame.grid(row=6, column=0, sticky="ew") @@ -585,7 +590,6 @@ class MainApplication(tk.Tk): if mode_when_started != self.mode: if calc_type == 'accurate_incremental': self.actions._set_ui_state(True) - self.genaue_berechnung_var.set(False) self.accurate_calculation_running = False self.animated_icon.stop("DISABLE") else: @@ -634,7 +638,6 @@ class MainApplication(tk.Tk): self.task_progress.config( mode="determinate", value=0) self.actions._set_ui_state(True) - self.genaue_berechnung_var.set(False) self.accurate_calculation_running = False self.start_pause_button.config( text=Msg.STR["start"]) diff --git a/pyimage_ui/actions.py b/pyimage_ui/actions.py index 0ba0d0f..0e41cef 100644 --- a/pyimage_ui/actions.py +++ b/pyimage_ui/actions.py @@ -73,7 +73,7 @@ class Actions: self.app.incremental_cb.config(state="normal") self.app.compressed_cb.config(state="normal") self.app.encrypted_cb.config(state="normal") - self.app.accurate_size_cb.config(state="normal") + self.app.accurate_size_btn.config(state="normal") # Apply specific logic based on current states if self.app.compressed_var.get(): @@ -82,8 +82,7 @@ class Actions: self.app.incremental_cb.config(state="disabled") self.app.encrypted_var.set(False) # If compressed, cannot be encrypted self.app.encrypted_cb.config(state="disabled") - self.app.accurate_size_cb.config(state="disabled") # Accurate size not applicable for compressed - self.app.genaue_berechnung_var.set(False) + self.app.accurate_size_btn.config(state="disabled") # Accurate size not applicable for compressed elif self.app.encrypted_var.get(): self.app.compressed_var.set(False) # If encrypted, cannot be compressed self.app.compressed_cb.config(state="disabled") @@ -108,9 +107,7 @@ class Actions: def handle_encryption_change(self): self._refresh_backup_options_ui() - def on_toggle_accurate_size_calc(self): - if not self.app.genaue_berechnung_var.get(): - return + def on_accurate_size_calc(self): if self.app.calculation_thread and self.app.calculation_thread.is_alive(): self.app.calculation_stop_event.set() @@ -141,7 +138,6 @@ class Actions: app_logger.log( "Cannot start accurate calculation, source folder info missing.") self._set_ui_state(True) - self.app.genaue_berechnung_var.set(False) self.app.accurate_calculation_running = False self.app.animated_icon.stop("DISABLE") return @@ -235,12 +231,6 @@ class Actions: else: extra_info = Msg.STR["user_restore_info"] - if self.app.mode == 'backup': - self._update_backup_type_controls() - else: - self.app.config_manager.set_setting( - "restore_destination_path", folder_path) - self._start_left_canvas_calculation( button_text, str(folder_path), icon_name, extra_info) self.app._update_sync_mode_display() @@ -375,7 +365,6 @@ class Actions: current_source = self.app.left_canvas_data.get('folder') if current_source: self.on_sidebar_button_click(current_source) - self._update_backup_type_controls() elif self.app.mode == "restore": self.app.right_canvas_data.update({ @@ -491,17 +480,16 @@ class Actions: if enable: self.app.update_backup_options_from_config() - self.app.actions._update_backup_type_controls() else: checkboxes = [ self.app.full_backup_cb, self.app.incremental_cb, - self.app.accurate_size_cb, self.app.compressed_cb, self.app.encrypted_cb, self.app.test_run_cb, self.app.bypass_security_cb ] + self.app.accurate_size_btn.config(state="disabled") for cb in checkboxes: cb.config(state="disabled") @@ -512,7 +500,6 @@ class Actions: if self.app.accurate_calculation_running: app_logger.log("Accurate size calculation cancelled by user.") self.app.accurate_calculation_running = False - self.app.genaue_berechnung_var.set(False) self.app.animated_icon.stop("DISABLE") if self.app.left_canvas_animation: diff --git a/pyimage_ui/advanced_settings_frame.py b/pyimage_ui/advanced_settings_frame.py index 255e2f4..543646c 100644 --- a/pyimage_ui/advanced_settings_frame.py +++ b/pyimage_ui/advanced_settings_frame.py @@ -126,17 +126,17 @@ class AdvancedSettingsFrame(ttk.Frame): self.no_trash_bin_var = tk.BooleanVar() self.full_backup_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_full_backup"], variable=self.force_full_var, command=lambda: enforce_backup_type_exclusivity( - self.force_full_var, self.force_incremental_var, self.force_full_var.get())) + self.force_full_var, self.force_incremental_var, self.force_full_var.get()), style="Switch.TCheckbutton") self.full_backup_checkbutton.pack(anchor=tk.W) self.incremental_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_incremental_backup"], variable=self.force_incremental_var, command=lambda: enforce_backup_type_exclusivity( - self.force_incremental_var, self.force_full_var, self.force_incremental_var.get())) + self.force_incremental_var, self.force_full_var, self.force_incremental_var.get()), style="Switch.TCheckbutton") self.incremental_checkbutton.pack(anchor=tk.W) - self.compression_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_compression"], variable=self.force_compression_var, command=self._on_compression_toggle) + self.compression_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_compression"], variable=self.force_compression_var, command=self._on_compression_toggle, style="Switch.TCheckbutton") self.compression_checkbutton.pack(anchor=tk.W) - self.encryption_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_encryption"], variable=self.force_encryption_var) + self.encryption_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_encryption"], variable=self.force_encryption_var, style="Switch.TCheckbutton") self.encryption_checkbutton.pack(anchor=tk.W) ttk.Separator(self.backup_defaults_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) @@ -145,9 +145,9 @@ class AdvancedSettingsFrame(ttk.Frame): trash_info_label.pack(anchor=tk.W, pady=5) ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["use_trash_bin"], variable=self.use_trash_bin_var, command=lambda: self._handle_trash_checkbox_click( - self.use_trash_bin_var, self.no_trash_bin_var)).pack(anchor=tk.W) + self.use_trash_bin_var, self.no_trash_bin_var), style="Switch.TCheckbutton").pack(anchor=tk.W) ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["no_trash_bin"], variable=self.no_trash_bin_var, command=lambda: self._handle_trash_checkbox_click( - self.no_trash_bin_var, self.use_trash_bin_var)).pack(anchor=tk.W) + self.no_trash_bin_var, self.use_trash_bin_var), style="Switch.TCheckbutton").pack(anchor=tk.W) ttk.Separator(self.backup_defaults_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) @@ -546,4 +546,4 @@ class AdvancedSettingsFrame(ttk.Frame): user_patterns.extend( [line.strip() for line in f if line.strip() and not line.startswith('#')]) - return generated_patterns, user_patterns \ No newline at end of file + return generated_patterns, user_patterns diff --git a/pyimage_ui/navigation.py b/pyimage_ui/navigation.py index cdcd2ec..d9f876b 100644 --- a/pyimage_ui/navigation.py +++ b/pyimage_ui/navigation.py @@ -112,8 +112,6 @@ class Navigation: self.app.encrypted_cb.config(state="normal") self.app.bypass_security_cb.config( state='disabled') # This one is mode-dependent - # Let the central config function handle the state of these checkboxes - self.app.update_backup_options_from_config() else: # restore self.app.source_size_frame.grid_remove() self.app.target_size_frame.grid_remove() diff --git a/pyimage_ui/scheduler_frame.py b/pyimage_ui/scheduler_frame.py index 05c1e8f..5a92807 100644 --- a/pyimage_ui/scheduler_frame.py +++ b/pyimage_ui/scheduler_frame.py @@ -53,13 +53,20 @@ class SchedulerFrame(ttk.Frame): } self.frequency = tk.StringVar(value="daily") + self.backup_type_system_var = tk.BooleanVar(value=True) + self.backup_type_user_var = tk.BooleanVar(value=False) + + self.freq_daily_var = tk.BooleanVar(value=True) + self.freq_weekly_var = tk.BooleanVar(value=False) + self.freq_monthly_var = tk.BooleanVar(value=False) + type_frame = ttk.LabelFrame( self.add_job_frame, text=Msg.STR["backup_type"], padding=10) type_frame.pack(fill=tk.X, padx=5, pady=5) - ttk.Radiobutton(type_frame, text=Msg.STR["system_backup_menu"], variable=self.backup_type, - value="system", command=self._toggle_user_sources).pack(anchor=tk.W) - ttk.Radiobutton(type_frame, text=Msg.STR["user_backup_menu"], variable=self.backup_type, - value="user", command=self._toggle_user_sources).pack(anchor=tk.W) + ttk.Checkbutton(type_frame, text=Msg.STR["system_backup_menu"], variable=self.backup_type_system_var, + style="Switch.TCheckbutton", command=lambda: self._handle_backup_type_switch("system")).pack(anchor=tk.W) + ttk.Checkbutton(type_frame, text=Msg.STR["user_backup_menu"], variable=self.backup_type_user_var, + style="Switch.TCheckbutton", command=lambda: self._handle_backup_type_switch("user")).pack(anchor=tk.W) # Container for source folders and backup options source_options_container = ttk.Frame(self.add_job_frame) @@ -72,7 +79,7 @@ class SchedulerFrame(ttk.Frame): self.user_sources_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) for name, var in self.user_sources.items(): ttk.Checkbutton(self.user_sources_frame, text=name, - variable=var).pack(anchor=tk.W) + variable=var, style="Switch.TCheckbutton").pack(anchor=tk.W) options_frame = ttk.LabelFrame( source_options_container, text=Msg.STR["backup_options"], padding=10) @@ -83,13 +90,13 @@ class SchedulerFrame(ttk.Frame): self.compress_var = tk.BooleanVar(value=False) self.encrypt_var = tk.BooleanVar(value=False) - self.full_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["full_backup"], variable=self.full_var, command=lambda: enforce_backup_type_exclusivity(self.full_var, self.incremental_var, self.full_var.get())) + self.full_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["full_backup"], variable=self.full_var, command=lambda: enforce_backup_type_exclusivity(self.full_var, self.incremental_var, self.full_var.get()), style="Switch.TCheckbutton") self.full_checkbutton.pack(anchor=tk.W) - self.incremental_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["incremental_backup"], variable=self.incremental_var, command=lambda: enforce_backup_type_exclusivity(self.incremental_var, self.full_var, self.incremental_var.get())) + self.incremental_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["incremental_backup"], variable=self.incremental_var, command=lambda: enforce_backup_type_exclusivity(self.incremental_var, self.full_var, self.incremental_var.get()), style="Switch.TCheckbutton") self.incremental_checkbutton.pack(anchor=tk.W) - self.compress_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["compression"], variable=self.compress_var, command=self._on_compression_toggle_scheduler) + self.compress_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["compression"], variable=self.compress_var, command=self._on_compression_toggle_scheduler, style="Switch.TCheckbutton") self.compress_checkbutton.pack(anchor=tk.W) - self.encrypt_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["encryption"], variable=self.encrypt_var) + self.encrypt_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["encryption"], variable=self.encrypt_var, style="Switch.TCheckbutton") self.encrypt_checkbutton.pack(anchor=tk.W) dest_frame = ttk.LabelFrame( @@ -103,12 +110,12 @@ class SchedulerFrame(ttk.Frame): freq_frame = ttk.LabelFrame( self.add_job_frame, text=Msg.STR["frequency"], padding=10) freq_frame.pack(fill=tk.X, padx=5, pady=5) - ttk.Radiobutton(freq_frame, text=Msg.STR["freq_daily"], - variable=self.frequency, value="daily").pack(anchor=tk.W) - ttk.Radiobutton(freq_frame, text=Msg.STR["freq_weekly"], - variable=self.frequency, value="weekly").pack(anchor=tk.W) - ttk.Radiobutton(freq_frame, text=Msg.STR["freq_monthly"], - variable=self.frequency, value="monthly").pack(anchor=tk.W) + ttk.Checkbutton(freq_frame, text=Msg.STR["freq_daily"], variable=self.freq_daily_var, + style="Switch.TCheckbutton", command=lambda: self._handle_freq_switch("daily")).pack(anchor=tk.W) + ttk.Checkbutton(freq_frame, text=Msg.STR["freq_weekly"], variable=self.freq_weekly_var, + style="Switch.TCheckbutton", command=lambda: self._handle_freq_switch("weekly")).pack(anchor=tk.W) + ttk.Checkbutton(freq_frame, text=Msg.STR["freq_monthly"], variable=self.freq_monthly_var, + style="Switch.TCheckbutton", command=lambda: self._handle_freq_switch("monthly")).pack(anchor=tk.W) add_button_frame = ttk.Frame(self.add_job_frame) add_button_frame.pack(pady=10) @@ -123,6 +130,33 @@ class SchedulerFrame(ttk.Frame): # Initially, hide the add_job_frame self.add_job_frame.pack_forget() + def _handle_backup_type_switch(self, changed_var): + if changed_var == "system": + if self.backup_type_system_var.get(): + self.backup_type_user_var.set(False) + self.backup_type.set("system") + else: + # Prevent unsetting both + self.backup_type_system_var.set(True) + elif changed_var == "user": + if self.backup_type_user_var.get(): + self.backup_type_system_var.set(False) + self.backup_type.set("user") + else: + self.backup_type_user_var.set(True) + self._toggle_user_sources() + + def _handle_freq_switch(self, changed_var): + vars = {"daily": self.freq_daily_var, "weekly": self.freq_weekly_var, "monthly": self.freq_monthly_var} + if vars[changed_var].get(): + self.frequency.set(changed_var) + for var_name, var_obj in vars.items(): + if var_name != changed_var: + var_obj.set(False) + else: + # Prevent unsetting all + vars[changed_var].set(True) + def show(self): self.grid(row=2, column=0, sticky="nsew") self._load_scheduled_jobs() @@ -255,4 +289,4 @@ class SchedulerFrame(ttk.Frame): job_id = self.jobs_tree.item(selected_item)["values"][0] self.backup_manager.remove_scheduled_job(job_id) - self._load_scheduled_jobs() + self._load_scheduled_jobs() \ No newline at end of file