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.
292 lines
13 KiB
Python
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() |