feat(ui): Ersetze Checkboxen und Radio-Buttons durch Switches

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.
This commit is contained in:
2025-09-10 01:04:10 +02:00
parent fd6bb6cc1b
commit b6a0bb82f1
6 changed files with 91 additions and 67 deletions

View File

@@ -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:

View File

@@ -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"])

View File

@@ -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:

View File

@@ -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
return generated_patterns, user_patterns

View File

@@ -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()

View File

@@ -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()