Files
Py-Backup/pyimage_ui/scheduler_frame.py
Désiré Werner Menrath b6a0bb82f1 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.
2025-09-10 01:04:10 +02:00

292 lines
13 KiB
Python

import tkinter as tk
from tkinter import ttk
import os
from shared_libs.custom_file_dialog import CustomFileDialog
from shared_libs.message import MessageDialog
from core.pbp_app_config import Msg
from pyimage_ui.shared_logic import enforce_backup_type_exclusivity
class SchedulerFrame(ttk.Frame):
def __init__(self, master, backup_manager, **kwargs):
super().__init__(master, **kwargs)
self.backup_manager = backup_manager
# --- Jobs List View ---
self.jobs_frame = ttk.LabelFrame(
self, text=Msg.STR["scheduled_jobs"], padding=10)
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
columns = ("active", "type", "frequency", "destination", "sources", "options")
self.jobs_tree = ttk.Treeview(
self.jobs_frame, columns=columns, show="headings")
self.jobs_tree.heading("active", text=Msg.STR["active"])
self.jobs_tree.heading("type", text=Msg.STR["col_type"])
self.jobs_tree.heading("frequency", text=Msg.STR["frequency"])
self.jobs_tree.heading("destination", text=Msg.STR["destination"])
self.jobs_tree.heading("sources", text=Msg.STR["sources"])
self.jobs_tree.heading("options", text=Msg.STR["backup_options"])
for col in columns:
self.jobs_tree.column(col, width=100, anchor="center")
self.jobs_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self._load_scheduled_jobs()
list_button_frame = ttk.Frame(self.jobs_frame)
list_button_frame.pack(pady=10)
ttk.Button(list_button_frame, text=Msg.STR["add"],
command=self._toggle_scheduler_view).pack(side=tk.LEFT, padx=5)
ttk.Button(list_button_frame, text=Msg.STR["remove"],
command=self._remove_selected_job).pack(side=tk.LEFT, padx=5)
# --- Add Job View ---
self.add_job_frame = ttk.LabelFrame(
self, text=Msg.STR["add_job_title"], padding=10)
self.backup_type = tk.StringVar(value="system")
self.destination = tk.StringVar()
self.user_sources = {
Msg.STR["cat_images"]: tk.BooleanVar(value=False),
Msg.STR["cat_documents"]: tk.BooleanVar(value=False),
Msg.STR["cat_music"]: tk.BooleanVar(value=False),
Msg.STR["cat_videos"]: tk.BooleanVar(value=False)
}
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.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)
source_options_container.pack(fill=tk.X, padx=5, pady=5)
source_options_container.columnconfigure(0, weight=1)
source_options_container.columnconfigure(1, weight=1)
self.user_sources_frame = ttk.LabelFrame(
source_options_container, text=Msg.STR["source_folders"], padding=10)
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, style="Switch.TCheckbutton").pack(anchor=tk.W)
options_frame = ttk.LabelFrame(
source_options_container, text=Msg.STR["backup_options"], padding=10)
options_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5)
self.full_var = tk.BooleanVar(value=True)
self.incremental_var = tk.BooleanVar(value=False)
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()), 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()), 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, 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, style="Switch.TCheckbutton")
self.encrypt_checkbutton.pack(anchor=tk.W)
dest_frame = ttk.LabelFrame(
self.add_job_frame, text=Msg.STR["dest_folder"], padding=10)
dest_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Entry(dest_frame, textvariable=self.destination, state="readonly",
width=50).pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Button(dest_frame, text=Msg.STR["browse"], command=self._select_destination).pack(
side=tk.RIGHT)
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.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)
ttk.Button(add_button_frame, text=Msg.STR["save"], command=self._save_job).pack(
side=tk.LEFT, padx=5)
ttk.Button(add_button_frame, text=Msg.STR["back"],
command=self._toggle_scheduler_view).pack(side=tk.LEFT, padx=5)
# Set initial state for the user sources
self._toggle_user_sources()
# 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()
def hide(self):
self.grid_remove()
def _on_compression_toggle_scheduler(self):
if self.compress_var.get():
self.incremental_var.set(False)
self.incremental_checkbutton.config(state="disabled")
self.encrypt_var.set(False)
self.encrypt_checkbutton.config(state="disabled")
else:
self.incremental_checkbutton.config(state="normal")
self.encrypt_checkbutton.config(state="normal")
def _toggle_scheduler_view(self):
if self.jobs_frame.winfo_ismapped():
self.jobs_frame.pack_forget()
self.add_job_frame.pack(fill=tk.BOTH, expand=True)
# Reset or load default values for new job
self.full_var.set(True)
self.incremental_var.set(False)
self.compress_var.set(False)
self.encrypt_var.set(False)
self.destination.set("")
for var in self.user_sources.values():
var.set(False)
self._on_compression_toggle_scheduler() # Update state of incremental checkbox
else:
self.add_job_frame.pack_forget()
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
self._load_scheduled_jobs()
def _toggle_user_sources(self):
state = "normal" if self.backup_type.get() == "user" else "disabled"
for child in self.user_sources_frame.winfo_children():
child.configure(state=state)
def _select_destination(self):
dialog = CustomFileDialog(
self, mode="dir", title=Msg.STR["select_dest_folder_title"])
result = dialog.get_result()
if result:
self.destination.set(result)
def _save_job(self):
dest = self.destination.get()
if not dest:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
return
job_type = self.backup_type.get()
job_frequency = self.frequency.get()
job_sources = []
if job_type == "user":
job_sources = [name for name,
var in self.user_sources.items() if var.get()]
if not job_sources:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_source_folder"])
return
script_path = os.path.abspath(os.path.join(
os.path.dirname(__file__), "..", "pybackup-cli.py"))
command = f"python3 {script_path} --backup-type {job_type} --destination \"{dest}\""
if self.full_var.get():
command += " --full"
if self.incremental_var.get():
command += " --incremental"
if self.compress_var.get():
command += " --compress"
if self.encrypt_var.get():
command += " --encrypt"
if job_type == "user":
command += f" --sources "
for s in job_sources:
command += f'\"{s}\" '
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
if job_type == "user":
comment += f"; sources:{','.join(job_sources)}"
comment += f"; full:{self.full_var.get()}; incremental:{self.incremental_var.get()}; compress:{self.compress_var.get()}; encrypt:{self.encrypt_var.get()}"
job_details = {
"command": command,
"comment": comment,
"type": job_type,
"frequency": job_frequency,
"destination": dest,
"sources": job_sources
}
self.backup_manager.add_scheduled_job(job_details)
self._load_scheduled_jobs()
self._toggle_scheduler_view()
def _load_scheduled_jobs(self):
for i in self.jobs_tree.get_children():
self.jobs_tree.delete(i)
jobs = self.backup_manager.get_scheduled_jobs()
for job in jobs:
options = []
if job.get("full"):
options.append("Full")
if job.get("incremental"):
options.append("Incremental")
if job.get("compress"):
options.append("Compressed")
if job.get("encrypt"):
options.append("Encrypted")
self.jobs_tree.insert("", "end", values=(
job["active"], job["type"], job["frequency"], job["destination"], ", ".join(
job["sources"]), ", ".join(options)
), iid=job["id"])
def _remove_selected_job(self):
selected_item = self.jobs_tree.focus()
if not selected_item:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_job_selected"])
return
job_id = self.jobs_tree.item(selected_item)["values"][0]
self.backup_manager.remove_scheduled_job(job_id)
self._load_scheduled_jobs()