Auto Copy Folder - Source Code
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import shutil
import threading
import time
import json
import os
from datetime import datetime, timedelta
import pystray
from PIL import Image, ImageDraw
import sys
import winshell
from win32com.client import Dispatch
import keyboard
import subprocess
class FolderCopierTool:
def __init__(self, root):
self.root = root
self.root.title('Folder Copier Tool')
self.root.geometry('1200x900')
self.root.resizable(True, True)
# Copy Configuration variables
self.copy_pairs = []
self.timer_running = False
self.interval = 5
self.show_notifications = False
self.disable_logging = False
self.start_as_icon = False
self.auto_start = True
self.overwrite_files = False
self.copy_thread = None
# Auto Saver variables
self.auto_saver_pairs = []
self.saver_hours = 0
self.saver_minutes = 0
self.saver_seconds = 30
self.saver_running = False
self.saver_thread = None
self.last_save_time = None
self.auto_start_saver = False
self.saver_next_save_time = None
self.countdown_update_job = None
# Manual saver variables
self.manual_saver_pairs = [] # List of (source, dest, hotkey)
self.registered_hotkeys = {} # Dict of {hotkey: index}
# Common variables
self.tray_icon = None
self.is_minimized_to_tray = False
self.config_file = 'copier_config.json'
# Initialize variables before load_config
self.disable_log_var = tk.BooleanVar()
self.start_as_icon_var = tk.BooleanVar()
self.auto_start_var = tk.BooleanVar()
self.notify_var = tk.BooleanVar()
self.overwrite_var = tk.BooleanVar(value=self.overwrite_files)
self.auto_start_saver_var = tk.BooleanVar()
self.load_config()
self.create_widgets()
if self.start_as_icon:
self.root.after(100, self.minimize_to_tray)
if self.auto_start and self.copy_pairs:
self.root.after(500, self.start_timer)
# Auto start saver if enabled
if self.auto_start_saver and self.auto_saver_pairs:
self.root.after(500, self.start_auto_saver)
# Register all manual saver hotkeys
self.register_all_manual_hotkeys()
def create_widgets(self):
notebook = ttk.Notebook(self.root)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Copy Configuration Tab
main_frame = ttk.Frame(notebook, padding='10')
notebook.add(main_frame, text='Copy Configuration')
main_frame.columnconfigure(1, weight=1)
ttk.Label(main_frame, text='Source -> Destination Folder Pairs:',
font=('Arial', 10, 'bold')).grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=(0, 5))
columns = ('Source', 'Destination')
self.pairs_tree = ttk.Treeview(main_frame, columns=columns, show='headings', height=8)
self.pairs_tree.heading('Source', text='Source Folder')
self.pairs_tree.heading('Destination', text='Destination Folder')
self.pairs_tree.column('Source', width=400)
self.pairs_tree.column('Destination', width=400)
self.pairs_tree.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
# Right-click menu for copy pairs
self.copy_context_menu = tk.Menu(self.root, tearoff=0)
self.copy_context_menu.add_command(label="Open Source Folder", command=self.open_copy_source_folder)
self.copy_context_menu.add_command(label="Open Destination Folder", command=self.open_copy_destination_folder)
self.pairs_tree.bind("<Button-3>", self.show_copy_context_menu)
tree_scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.pairs_tree.yview)
tree_scrollbar.grid(row=1, column=3, sticky=(tk.N, tk.S), pady=(0, 10))
self.pairs_tree.configure(yscrollcommand=tree_scrollbar.set)
for source, dest in self.copy_pairs:
self.pairs_tree.insert('', 'end', values=(source, dest))
pairs_buttons = ttk.Frame(main_frame)
pairs_buttons.grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=(0, 20))
ttk.Button(pairs_buttons, text='Add Pair', command=self.add_copy_pair).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(pairs_buttons, text='Edit Pair', command=self.edit_copy_pair).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(pairs_buttons, text='Remove Pair', command=self.remove_copy_pair).pack(side=tk.LEFT)
# Timer Settings
timer_frame = ttk.LabelFrame(main_frame, text='Timer Settings', padding='10')
timer_frame.grid(row=3, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(0, 10))
timer_frame.columnconfigure(1, weight=1)
ttk.Label(timer_frame, text='Copy Interval:').grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
self.interval_var = tk.StringVar(value=str(self.interval))
intervals = [('5 seconds', '5'), ('15 seconds', '15'), ('30 seconds', '30'),
('1 minute', '60'), ('5 minutes', '300'), ('15 minutes', '900'),
('30 minutes', '1800'), ('1 hour', '3600'), ('2 hours', '7200'), ('3 hours', '10800')]
interval_combo = ttk.Combobox(timer_frame, textvariable=self.interval_var,
values=[val[1] for val in intervals], state='readonly')
interval_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=(0, 5), padx=(5, 0))
self.interval_desc = tk.StringVar()
for text, value in intervals:
if value == self.interval_var.get():
self.interval_desc.set(text)
break
ttk.Label(timer_frame, textvariable=self.interval_desc).grid(row=0, column=2, sticky=tk.W, padx=(5, 0))
interval_combo.bind('<<ComboboxSelected>>', self.on_interval_change)
self.notify_var.set(self.show_notifications)
notify_check = ttk.Checkbutton(timer_frame, text='Show Copy Completion Notifications',
variable=self.notify_var, command=self.on_notify_change)
notify_check.grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))
self.start_as_icon_var.set(self.start_as_icon)
start_icon_check = ttk.Checkbutton(timer_frame, text='Start as Icon (Launch in system tray)',
variable=self.start_as_icon_var, command=self.on_start_as_icon_change)
start_icon_check.grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))
self.auto_start_var.set(self.auto_start)
auto_start_check = ttk.Checkbutton(timer_frame, text='Auto Start (Automatic copy on launch)',
variable=self.auto_start_var, command=self.on_auto_start_change)
auto_start_check.grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))
self.overwrite_var.set(self.overwrite_files)
overwrite_check = ttk.Checkbutton(timer_frame, text='Overwrite Existing Files (NOT checked by default)',
variable=self.overwrite_var, command=self.on_overwrite_change)
overwrite_check.grid(row=4, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))
ttk.Button(timer_frame, text='Create Auto Launch at PC Startup',
command=self.create_auto_launch_shortcut).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))
# Control buttons
control_frame = ttk.Frame(main_frame)
control_frame.grid(row=4, column=0, columnspan=4, pady=20)
self.start_button = ttk.Button(control_frame, text='Start Automatic Copy', command=self.start_timer)
self.start_button.pack(side=tk.LEFT, padx=(0, 10))
self.stop_button = ttk.Button(control_frame, text='Stop Automatic Copy', command=self.stop_timer, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(control_frame, text='Copy Now', command=self.copy_now).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(control_frame, text='Minimize to Tray', command=self.minimize_to_tray).pack(side=tk.LEFT)
# Status
status_frame = ttk.Frame(main_frame)
status_frame.grid(row=5, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(10, 0))
ttk.Label(status_frame, text='Status:').pack(side=tk.LEFT)
self.status_var = tk.StringVar(value='Ready')
ttk.Label(status_frame, textvariable=self.status_var, foreground='blue').pack(side=tk.LEFT, padx=(5, 0))
# Saver Tab
saver_frame = ttk.Frame(notebook, padding='10')
notebook.add(saver_frame, text='Saver')
# Create a canvas with scrollbar for saver tab
saver_canvas = tk.Canvas(saver_frame)
saver_scrollbar = ttk.Scrollbar(saver_frame, orient="vertical", command=saver_canvas.yview)
saver_scrollable_frame = ttk.Frame(saver_canvas)
saver_scrollable_frame.bind(
"<Configure>",
lambda e: saver_canvas.configure(scrollregion=saver_canvas.bbox("all"))
)
saver_canvas.create_window((0, 0), window=saver_scrollable_frame, anchor="nw")
saver_canvas.configure(yscrollcommand=saver_scrollbar.set)
saver_canvas.pack(side="left", fill="both", expand=True)
saver_scrollbar.pack(side="right", fill="y")
# Automatic Folder Saver Section
auto_saver_section = ttk.LabelFrame(saver_scrollable_frame, text='Automatic Folder Saver', padding='10')
auto_saver_section.pack(fill=tk.BOTH, expand=False, pady=(0, 15))
# Auto Start Saver checkbox
self.auto_start_saver_var.set(self.auto_start_saver)
auto_start_saver_check = ttk.Checkbutton(auto_saver_section,
text='Auto Start Saver when Open',
variable=self.auto_start_saver_var,
command=self.on_auto_start_saver_change)
auto_start_saver_check.pack(anchor=tk.W, pady=(0, 10))
# Auto Saver Pairs List
ttk.Label(auto_saver_section, text='Source -> Destination Pairs:',
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
auto_columns = ('Source', 'Destination')
self.auto_saver_tree = ttk.Treeview(auto_saver_section, columns=auto_columns, show='headings', height=6)
self.auto_saver_tree.heading('Source', text='Source Folder')
self.auto_saver_tree.heading('Destination', text='Destination Folder')
self.auto_saver_tree.column('Source', width=400)
self.auto_saver_tree.column('Destination', width=400)
self.auto_saver_tree.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Right-click menu for auto saver pairs
self.auto_saver_context_menu = tk.Menu(self.root, tearoff=0)
self.auto_saver_context_menu.add_command(label="Open Source Folder", command=self.open_auto_saver_source_folder)
self.auto_saver_context_menu.add_command(label="Open Destination Folder", command=self.open_auto_saver_destination_folder)
self.auto_saver_tree.bind("<Button-3>", self.show_auto_saver_context_menu)
for source, dest in self.auto_saver_pairs:
self.auto_saver_tree.insert('', 'end', values=(source, dest))
auto_pairs_buttons = ttk.Frame(auto_saver_section)
auto_pairs_buttons.pack(anchor=tk.W, pady=(0, 10))
ttk.Button(auto_pairs_buttons, text='Add Pair', command=self.add_auto_saver_pair).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(auto_pairs_buttons, text='Edit Pair', command=self.edit_auto_saver_pair).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(auto_pairs_buttons, text='Remove Pair', command=self.remove_auto_saver_pair).pack(side=tk.LEFT)
# Time interval settings
time_frame = ttk.LabelFrame(auto_saver_section, text='Save Interval', padding='10')
time_frame.pack(fill=tk.X, pady=(0, 10))
time_controls = ttk.Frame(time_frame)
time_controls.pack()
ttk.Label(time_controls, text='Hours:').pack(side=tk.LEFT, padx=(0, 5))
self.saver_hours_var = tk.StringVar(value=str(self.saver_hours))
hours_spin = ttk.Spinbox(time_controls, from_=0, to=23, textvariable=self.saver_hours_var, width=5)
hours_spin.pack(side=tk.LEFT, padx=(0, 15))
ttk.Label(time_controls, text='Minutes:').pack(side=tk.LEFT, padx=(0, 5))
self.saver_minutes_var = tk.StringVar(value=str(self.saver_minutes))
minutes_spin = ttk.Spinbox(time_controls, from_=0, to=59, textvariable=self.saver_minutes_var, width=5)
minutes_spin.pack(side=tk.LEFT, padx=(0, 15))
ttk.Label(time_controls, text='Seconds:').pack(side=tk.LEFT, padx=(0, 5))
self.saver_seconds_var = tk.StringVar(value=str(self.saver_seconds))
seconds_spin = ttk.Spinbox(time_controls, from_=0, to=59, textvariable=self.saver_seconds_var, width=5)
seconds_spin.pack(side=tk.LEFT)
# Control buttons for auto saver
saver_control_frame = ttk.Frame(auto_saver_section)
saver_control_frame.pack(pady=(10, 0))
self.saver_start_button = ttk.Button(saver_control_frame, text='Start Auto Saver',
command=self.start_auto_saver)
self.saver_start_button.pack(side=tk.LEFT, padx=(0, 10))
self.saver_stop_button = ttk.Button(saver_control_frame, text='Stop Auto Saver',
command=self.stop_auto_saver, state=tk.DISABLED)
self.saver_stop_button.pack(side=tk.LEFT)
# Status for auto saver
saver_status_frame = ttk.Frame(auto_saver_section)
saver_status_frame.pack(pady=(10, 0))
ttk.Label(saver_status_frame, text='Status:').pack(side=tk.LEFT)
self.saver_status_var = tk.StringVar(value='Stopped')
ttk.Label(saver_status_frame, textvariable=self.saver_status_var, foreground='blue').pack(side=tk.LEFT, padx=(5, 0))
# Countdown timer
countdown_frame = ttk.Frame(auto_saver_section)
countdown_frame.pack(pady=(10, 0))
ttk.Label(countdown_frame, text='Next Save In:').pack(side=tk.LEFT)
self.saver_countdown_var = tk.StringVar(value='--:--:--')
ttk.Label(countdown_frame, textvariable=self.saver_countdown_var,
foreground='green', font=('Arial', 12, 'bold')).pack(side=tk.LEFT, padx=(5, 0))
# Manual Save Section
manual_saver_section = ttk.LabelFrame(saver_scrollable_frame, text='Manual Save', padding='10')
manual_saver_section.pack(fill=tk.BOTH, expand=False, pady=(0, 15))
# Manual Saver Pairs List
ttk.Label(manual_saver_section, text='Source -> Destination Pairs with Hotkeys:',
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
manual_columns = ('Source', 'Destination', 'Hotkey')
self.manual_saver_tree = ttk.Treeview(manual_saver_section, columns=manual_columns, show='headings', height=6)
self.manual_saver_tree.heading('Source', text='Source Folder')
self.manual_saver_tree.heading('Destination', text='Destination Folder')
self.manual_saver_tree.heading('Hotkey', text='Hotkey')
self.manual_saver_tree.column('Source', width=350)
self.manual_saver_tree.column('Destination', width=350)
self.manual_saver_tree.column('Hotkey', width=100)
self.manual_saver_tree.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Right-click menu for manual saver pairs
self.manual_saver_context_menu = tk.Menu(self.root, tearoff=0)
self.manual_saver_context_menu.add_command(label="Open Source Folder", command=self.open_manual_saver_source_folder)
self.manual_saver_context_menu.add_command(label="Open Destination Folder", command=self.open_manual_saver_destination_folder)
self.manual_saver_context_menu.add_separator()
self.manual_saver_context_menu.add_command(label="Reverse Save (Dest → Source)", command=self.reverse_manual_save)
self.manual_saver_tree.bind("<Button-3>", self.show_manual_saver_context_menu)
for source, dest, hotkey in self.manual_saver_pairs:
self.manual_saver_tree.insert('', 'end', values=(source, dest, hotkey))
manual_pairs_buttons = ttk.Frame(manual_saver_section)
manual_pairs_buttons.pack(anchor=tk.W, pady=(0, 10))
ttk.Button(manual_pairs_buttons, text='Add Pair', command=self.add_manual_saver_pair).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(manual_pairs_buttons, text='Edit Pair', command=self.edit_manual_saver_pair).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(manual_pairs_buttons, text='Remove Pair', command=self.remove_manual_saver_pair).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(manual_pairs_buttons, text='Save Selected', command=self.manual_save_selected).pack(side=tk.LEFT)
# Hotkey status
hotkey_status_frame = ttk.Frame(manual_saver_section)
hotkey_status_frame.pack(pady=(10, 0))
ttk.Label(hotkey_status_frame, text='Registered Hotkeys:').pack(side=tk.LEFT)
self.hotkey_count_var = tk.StringVar(value='0')
ttk.Label(hotkey_status_frame, textvariable=self.hotkey_count_var, foreground='green').pack(side=tk.LEFT, padx=(5, 0))
# Log Tab
log_frame = ttk.Frame(notebook, padding='10')
notebook.add(log_frame, text='Log')
ttk.Label(log_frame, text='Activity Log:', font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
self.log_text = scrolledtext.ScrolledText(log_frame, height=30, state=tk.DISABLED, wrap=tk.WORD)
self.log_text.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
log_buttons = ttk.Frame(log_frame)
log_buttons.pack(fill=tk.X)
ttk.Button(log_buttons, text='Clear Log', command=self.clear_log).pack(side=tk.LEFT, padx=(0, 10))
self.disable_log_var.set(self.disable_logging)
disable_log_check = ttk.Checkbutton(log_buttons, text='Disable Logging',
variable=self.disable_log_var, command=self.on_disable_log_change)
disable_log_check.pack(side=tk.LEFT)
self.root.protocol('WM_DELETE_WINDOW', self.on_closing)
# Context menu methods for Copy Configuration
def show_copy_context_menu(self, event):
item = self.pairs_tree.identify_row(event.y)
if item:
self.pairs_tree.selection_set(item)
self.copy_context_menu.post(event.x_root, event.y_root)
def open_copy_source_folder(self):
selection = self.pairs_tree.selection()
if selection:
index = self.pairs_tree.index(selection[0])
source, _ = self.copy_pairs[index]
self.open_folder(source)
def open_copy_destination_folder(self):
selection = self.pairs_tree.selection()
if selection:
index = self.pairs_tree.index(selection[0])
_, dest = self.copy_pairs[index]
self.open_folder(dest)
# Context menu methods for Auto Saver
def show_auto_saver_context_menu(self, event):
item = self.auto_saver_tree.identify_row(event.y)
if item:
self.auto_saver_tree.selection_set(item)
self.auto_saver_context_menu.post(event.x_root, event.y_root)
def open_auto_saver_source_folder(self):
selection = self.auto_saver_tree.selection()
if selection:
index = self.auto_saver_tree.index(selection[0])
source, _ = self.auto_saver_pairs[index]
self.open_folder(source)
def open_auto_saver_destination_folder(self):
selection = self.auto_saver_tree.selection()
if selection:
index = self.auto_saver_tree.index(selection[0])
_, dest = self.auto_saver_pairs[index]
self.open_folder(dest)
# Context menu methods for Manual Saver
def show_manual_saver_context_menu(self, event):
item = self.manual_saver_tree.identify_row(event.y)
if item:
self.manual_saver_tree.selection_set(item)
self.manual_saver_context_menu.post(event.x_root, event.y_root)
def open_manual_saver_source_folder(self):
selection = self.manual_saver_tree.selection()
if selection:
index = self.manual_saver_tree.index(selection[0])
source, _, _ = self.manual_saver_pairs[index]
self.open_folder(source)
def open_manual_saver_destination_folder(self):
selection = self.manual_saver_tree.selection()
if selection:
index = self.manual_saver_tree.index(selection[0])
_, dest, _ = self.manual_saver_pairs[index]
self.open_folder(dest)
def open_folder(self, path):
try:
if os.path.exists(path):
if sys.platform == 'win32':
os.startfile(path)
elif sys.platform == 'darwin':
subprocess.Popen(['open', path])
else:
subprocess.Popen(['xdg-open', path])
else:
messagebox.showwarning('Warning', f'Folder does not exist:\n{path}')
except Exception as e:
messagebox.showerror('Error', f'Cannot open folder:\n{str(e)}')
# Auto Saver Pair Management
def add_auto_saver_pair(self):
source = filedialog.askdirectory(title='Select Source Folder for Auto Saver')
if not source:
return
dest = filedialog.askdirectory(title='Select Destination Folder for Auto Saver')
if not dest:
return
try:
source = os.path.normpath(source)
dest = os.path.normpath(dest)
except Exception as e:
messagebox.showerror('Error', f'Invalid path: {str(e)}')
return
self.auto_saver_pairs.append((source, dest))
self.auto_saver_tree.insert('', 'end', values=(source, dest))
self.save_config()
self.log_message(f'Added auto saver pair: {os.path.basename(source)} -> {os.path.basename(dest)}')
def edit_auto_saver_pair(self):
selection = self.auto_saver_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair to edit')
return
item = selection[0]
index = self.auto_saver_tree.index(item)
source = filedialog.askdirectory(title='Select New Source Folder for Auto Saver')
if not source:
return
dest = filedialog.askdirectory(title='Select New Destination Folder for Auto Saver')
if not dest:
return
try:
source = os.path.normpath(source)
dest = os.path.normpath(dest)
except Exception as e:
messagebox.showerror('Error', f'Invalid path: {str(e)}')
return
self.auto_saver_pairs[index] = (source, dest)
self.auto_saver_tree.item(item, values=(source, dest))
self.save_config()
self.log_message(f'Updated auto saver pair: {os.path.basename(source)} -> {os.path.basename(dest)}')
def remove_auto_saver_pair(self):
selection = self.auto_saver_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair to remove')
return
item = selection[0]
index = self.auto_saver_tree.index(item)
source, dest = self.auto_saver_pairs[index]
self.auto_saver_pairs.pop(index)
self.auto_saver_tree.delete(item)
self.save_config()
self.log_message(f'Removed auto saver pair: {os.path.basename(source)} -> {os.path.basename(dest)}')
# Manual Saver Pair Management
def add_manual_saver_pair(self):
source = filedialog.askdirectory(title='Select Source Folder for Manual Saver')
if not source:
return
dest = filedialog.askdirectory(title='Select Destination Folder for Manual Saver')
if not dest:
return
# Ask for hotkey
hotkey = self.ask_hotkey()
if not hotkey:
return
# Check if hotkey already exists
if hotkey in self.registered_hotkeys:
messagebox.showerror('Error', f'Hotkey "{hotkey}" is already in use')
return
try:
source = os.path.normpath(source)
dest = os.path.normpath(dest)
except Exception as e:
messagebox.showerror('Error', f'Invalid path: {str(e)}')
return
self.manual_saver_pairs.append((source, dest, hotkey))
self.manual_saver_tree.insert('', 'end', values=(source, dest, hotkey))
self.save_config()
# Register the hotkey
self.register_single_hotkey(len(self.manual_saver_pairs) - 1, hotkey)
self.log_message(f'Added manual saver pair: {os.path.basename(source)} -> {os.path.basename(dest)} [{hotkey}]')
def edit_manual_saver_pair(self):
selection = self.manual_saver_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair to edit')
return
item = selection[0]
index = self.manual_saver_tree.index(item)
old_source, old_dest, old_hotkey = self.manual_saver_pairs[index]
source = filedialog.askdirectory(title='Select New Source Folder for Manual Saver')
if not source:
return
dest = filedialog.askdirectory(title='Select New Destination Folder for Manual Saver')
if not dest:
return
# Ask for new hotkey
hotkey = self.ask_hotkey(default=old_hotkey)
if not hotkey:
return
# Check if hotkey already exists (and it's not the same hotkey)
if hotkey != old_hotkey and hotkey in self.registered_hotkeys:
messagebox.showerror('Error', f'Hotkey "{hotkey}" is already in use')
return
try:
source = os.path.normpath(source)
dest = os.path.normpath(dest)
except Exception as e:
messagebox.showerror('Error', f'Invalid path: {str(e)}')
return
# Unregister old hotkey if changed
if hotkey != old_hotkey:
self.unregister_single_hotkey(old_hotkey)
self.manual_saver_pairs[index] = (source, dest, hotkey)
self.manual_saver_tree.item(item, values=(source, dest, hotkey))
self.save_config()
# Register new hotkey if changed
if hotkey != old_hotkey:
self.register_single_hotkey(index, hotkey)
self.log_message(f'Updated manual saver pair: {os.path.basename(source)} -> {os.path.basename(dest)} [{hotkey}]')
def remove_manual_saver_pair(self):
selection = self.manual_saver_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair to remove')
return
item = selection[0]
index = self.manual_saver_tree.index(item)
source, dest, hotkey = self.manual_saver_pairs[index]
# Unregister hotkey
self.unregister_single_hotkey(hotkey)
self.manual_saver_pairs.pop(index)
self.manual_saver_tree.delete(item)
self.save_config()
# Re-register all hotkeys with updated indices
self.unregister_all_hotkeys()
self.register_all_manual_hotkeys()
self.log_message(f'Removed manual saver pair: {os.path.basename(source)} -> {os.path.basename(dest)} [{hotkey}]')
def ask_hotkey(self, default=''):
dialog = tk.Toplevel(self.root)
dialog.title('Enter Hotkey')
dialog.geometry('300x120')
dialog.resizable(False, False)
dialog.transient(self.root)
dialog.grab_set()
ttk.Label(dialog, text='Enter hotkey (e.g., alt+h, ctrl+shift+s):').pack(pady=(10, 5))
hotkey_var = tk.StringVar(value=default)
entry = ttk.Entry(dialog, textvariable=hotkey_var, width=30)
entry.pack(pady=5)
entry.focus()
result = {'hotkey': None}
def on_ok():
result['hotkey'] = hotkey_var.get().strip().lower()
dialog.destroy()
def on_cancel():
dialog.destroy()
button_frame = ttk.Frame(dialog)
button_frame.pack(pady=10)
ttk.Button(button_frame, text='OK', command=on_ok).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text='Cancel', command=on_cancel).pack(side=tk.LEFT, padx=5)
entry.bind('<Return>', lambda e: on_ok())
entry.bind('<Escape>', lambda e: on_cancel())
dialog.wait_window()
return result['hotkey']
def register_all_manual_hotkeys(self):
for i, (source, dest, hotkey) in enumerate(self.manual_saver_pairs):
self.register_single_hotkey(i, hotkey)
def register_single_hotkey(self, index, hotkey):
try:
keyboard.add_hotkey(hotkey, lambda idx=index: self.manual_save_by_index(idx))
self.registered_hotkeys[hotkey] = index
self.update_hotkey_count()
self.log_message(f'Hotkey registered: {hotkey}')
except Exception as e:
self.log_message(f'ERROR registering hotkey {hotkey}: {str(e)}')
def unregister_single_hotkey(self, hotkey):
if hotkey in self.registered_hotkeys:
try:
keyboard.remove_hotkey(hotkey)
del self.registered_hotkeys[hotkey]
self.update_hotkey_count()
except Exception as e:
self.log_message(f'ERROR unregistering hotkey {hotkey}: {str(e)}')
def unregister_all_hotkeys(self):
for hotkey in list(self.registered_hotkeys.keys()):
self.unregister_single_hotkey(hotkey)
def update_hotkey_count(self):
self.hotkey_count_var.set(str(len(self.registered_hotkeys)))
def manual_save_by_index(self, index):
if 0 <= index < len(self.manual_saver_pairs):
self.root.after(0, lambda: self.perform_manual_save(index))
def manual_save_selected(self):
selection = self.manual_saver_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair to save')
return
index = self.manual_saver_tree.index(selection[0])
self.perform_manual_save(index)
def reverse_manual_save(self):
selection = self.manual_saver_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair for reverse save')
return
index = self.manual_saver_tree.index(selection[0])
result = messagebox.askyesno('Confirm Reverse Save',
'This will copy files from DESTINATION to SOURCE, overwriting existing files.\n\n'
'Are you sure you want to continue?')
if result:
self.perform_reverse_manual_save(index)
def perform_reverse_manual_save(self, index):
try:
source, dest, hotkey = self.manual_saver_pairs[index]
# Validate folders exist
if not os.path.exists(dest):
messagebox.showerror('Error', f'Destination folder does not exist:\n{dest}')
return
if not os.path.exists(source):
os.makedirs(source, exist_ok=True)
# Get the source folder name to identify the correct subfolder
source_folder_name = os.path.basename(source)
dest_subfolder = os.path.join(dest, source_folder_name)
if not os.path.exists(dest_subfolder):
messagebox.showerror('Error', f'No saves found for this source folder in:\n{dest_subfolder}')
return
# Get list of timestamped folders in the specific subfolder
timestamp_folders = [f for f in os.listdir(dest_subfolder)
if os.path.isdir(os.path.join(dest_subfolder, f)) and f.startswith('Manual_')]
if not timestamp_folders:
messagebox.showwarning('Warning', 'No manual save folders found for this source')
return
# Sort to get the most recent
timestamp_folders.sort(reverse=True)
most_recent = timestamp_folders[0]
# The Manual folder contains the saved content
reverse_source = os.path.join(dest_subfolder, most_recent)
# Get the first folder inside Manual_ (this is the saved source folder)
saved_folders = [f for f in os.listdir(reverse_source)
if os.path.isdir(os.path.join(reverse_source, f))]
if not saved_folders:
messagebox.showerror('Error', f'No folders found in:\n{reverse_source}')
return
# Use the first folder found (should be the source folder)
saved_folder_name = saved_folders[0]
actual_reverse_source = os.path.join(reverse_source, saved_folder_name)
# Copy files from destination back to source (with overwrite)
files_copied = 0
for root, dirs, files in os.walk(actual_reverse_source):
rel_path = os.path.relpath(root, actual_reverse_source)
dest_dir = os.path.join(source, rel_path)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir, exist_ok=True)
for file in files:
src_file = os.path.join(root, file)
dst_file = os.path.join(dest_dir, file)
try:
shutil.copy2(src_file, dst_file)
files_copied += 1
except Exception as e:
self.log_message(f'ERROR copying file in reverse save: {str(e)}')
self.log_message(f'Reverse Save completed: {files_copied} files restored from {most_recent} to source')
messagebox.showinfo('Success', f'Reverse save completed!\n{files_copied} files restored to source folder.')
except Exception as e:
self.log_message(f'ERROR in reverse save: {str(e)}')
messagebox.showerror('Error', f'Reverse save failed: {str(e)}')
def start_auto_saver(self):
try:
self.saver_hours = int(self.saver_hours_var.get())
self.saver_minutes = int(self.saver_minutes_var.get())
self.saver_seconds = int(self.saver_seconds_var.get())
except ValueError:
messagebox.showerror('Error', 'Please enter valid numbers for time interval')
return
# Validate inputs
if not self.auto_saver_pairs:
messagebox.showerror('Error', 'Please add at least one source-destination pair')
return
total_seconds = self.saver_hours * 3600 + self.saver_minutes * 60 + self.saver_seconds
if total_seconds <= 0:
messagebox.showerror('Error', 'Time interval must be greater than 0')
return
self.save_config()
self.saver_running = True
self.saver_start_button.config(state=tk.DISABLED)
self.saver_stop_button.config(state=tk.NORMAL)
self.saver_status_var.set('Running')
# Initialize next save time
self.saver_next_save_time = datetime.now() + timedelta(seconds=total_seconds)
self.log_message('Auto Saver started')
# Start countdown update
self.update_saver_countdown()
# Start the saver thread
self.saver_thread = threading.Thread(target=self.auto_saver_worker, daemon=True)
self.saver_thread.start()
def stop_auto_saver(self):
self.saver_running = False
self.saver_start_button.config(state=tk.NORMAL)
self.saver_stop_button.config(state=tk.DISABLED)
self.saver_status_var.set('Stopped')
self.saver_countdown_var.set('--:--:--')
# Cancel countdown update
if self.countdown_update_job:
self.root.after_cancel(self.countdown_update_job)
self.countdown_update_job = None
self.log_message('Auto Saver stopped')
def auto_saver_worker(self):
while self.saver_running:
total_seconds = self.saver_hours * 3600 + self.saver_minutes * 60 + self.saver_seconds
# Wait for the interval
for _ in range(total_seconds):
if not self.saver_running:
return
time.sleep(1)
# Perform the save
self.perform_auto_save()
# Update next save time
if self.saver_running:
self.saver_next_save_time = datetime.now() + timedelta(seconds=total_seconds)
def update_saver_countdown(self):
if not self.saver_running:
return
if self.saver_next_save_time:
now = datetime.now()
time_remaining = self.saver_next_save_time - now
if time_remaining.total_seconds() > 0:
hours, remainder = divmod(int(time_remaining.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
self.saver_countdown_var.set(f'{hours:02d}:{minutes:02d}:{seconds:02d}')
else:
self.saver_countdown_var.set('00:00:00')
# Schedule next update in 1 second
self.countdown_update_job = self.root.after(1000, self.update_saver_countdown)
def perform_auto_save(self):
try:
# Create timestamped folder name with "Auto" prefix
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
auto_folder_name = f'Auto_{timestamp}'
total_files = 0
# Save all pairs
for source, dest in self.auto_saver_pairs:
if not os.path.exists(source):
self.log_message(f'WARNING: Auto saver source folder does not exist: {source}')
continue
# Create a unique subfolder based on source folder name
source_folder_name = os.path.basename(source)
dest_with_subfolder = os.path.join(dest, source_folder_name)
dest_folder = os.path.join(dest_with_subfolder, auto_folder_name)
# Get the source folder name for the final destination
final_dest = os.path.join(dest_folder, source_folder_name)
# Create destination directory
os.makedirs(final_dest, exist_ok=True)
# Copy the entire folder
files_copied = 0
for root, dirs, files in os.walk(source):
rel_path = os.path.relpath(root, source)
dest_dir = os.path.join(final_dest, rel_path)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir, exist_ok=True)
for file in files:
src_file = os.path.join(root, file)
dst_file = os.path.join(dest_dir, file)
try:
shutil.copy2(src_file, dst_file)
files_copied += 1
except Exception as e:
self.log_message(f'ERROR copying file in auto save: {str(e)}')
total_files += files_copied
self.log_message(f'Auto saved {files_copied} files from {os.path.basename(source)}')
self.last_save_time = datetime.now()
self.log_message(f'Auto Save completed: {total_files} total files saved to {auto_folder_name}')
except Exception as e:
self.log_message(f'ERROR in auto save: {str(e)}')
def perform_manual_save(self, index):
try:
source, dest, hotkey = self.manual_saver_pairs[index]
# Validate inputs
if not os.path.exists(source):
messagebox.showerror('Error', f'Source folder does not exist:\n{source}')
return
# Create a unique subfolder based on source folder name
source_folder_name = os.path.basename(source)
dest_with_subfolder = os.path.join(dest, source_folder_name)
# Create timestamped folder name with "Manual" prefix
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
manual_folder_name = f'Manual_{timestamp}'
dest_folder = os.path.join(dest_with_subfolder, manual_folder_name)
# Get the source folder name for the final destination
final_dest = os.path.join(dest_folder, source_folder_name)
# Create destination directory
os.makedirs(final_dest, exist_ok=True)
# Copy the entire folder
files_copied = 0
for root, dirs, files in os.walk(source):
rel_path = os.path.relpath(root, source)
dest_dir = os.path.join(final_dest, rel_path)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir, exist_ok=True)
for file in files:
src_file = os.path.join(root, file)
dst_file = os.path.join(dest_dir, file)
try:
shutil.copy2(src_file, dst_file)
files_copied += 1
except Exception as e:
self.log_message(f'ERROR copying file in manual save: {str(e)}')
self.log_message(f'Manual Save completed [{hotkey}]: {files_copied} files saved to {dest_folder}')
except Exception as e:
self.log_message(f'ERROR in manual save: {str(e)}')
messagebox.showerror('Error', f'Manual save failed: {str(e)}')
def on_auto_start_saver_change(self):
self.auto_start_saver = self.auto_start_saver_var.get()
self.save_config()
def add_copy_pair(self):
source = filedialog.askdirectory(title='Select Source Folder')
if not source:
return
dest = filedialog.askdirectory(title='Select Destination Folder')
if not dest:
return
try:
source = os.path.normpath(source)
dest = os.path.normpath(dest)
except Exception as e:
messagebox.showerror('Error', f'Invalid path: {str(e)}')
return
for existing_source, _ in self.copy_pairs:
if os.path.normpath(existing_source) == source:
messagebox.showwarning('Warning', 'This source folder is already in the list')
return
self.copy_pairs.append((source, dest))
self.pairs_tree.insert('', 'end', values=(source, dest))
self.save_config()
self.log_message(f'Added copy pair: {os.path.basename(source)} -> {os.path.basename(dest)}')
def edit_copy_pair(self):
selection = self.pairs_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair to edit')
return
item = selection[0]
index = self.pairs_tree.index(item)
source = filedialog.askdirectory(title='Select New Source Folder')
if not source:
return
dest = filedialog.askdirectory(title='Select New Destination Folder')
if not dest:
return
try:
source = os.path.normpath(source)
dest = os.path.normpath(dest)
except Exception as e:
messagebox.showerror('Error', f'Invalid path: {str(e)}')
return
self.copy_pairs[index] = (source, dest)
self.pairs_tree.item(item, values=(source, dest))
self.save_config()
self.log_message(f'Updated copy pair: {os.path.basename(source)} -> {os.path.basename(dest)}')
def remove_copy_pair(self):
selection = self.pairs_tree.selection()
if not selection:
messagebox.showwarning('Warning', 'Please select a pair to remove')
return
item = selection[0]
index = self.pairs_tree.index(item)
source, dest = self.copy_pairs[index]
self.copy_pairs.pop(index)
self.pairs_tree.delete(item)
self.save_config()
self.log_message(f'Removed copy pair: {os.path.basename(source)} -> {os.path.basename(dest)}')
def on_interval_change(self, event):
try:
new_interval = int(self.interval_var.get())
self.interval = new_interval
intervals = [('5 seconds', '5'), ('15 seconds', '15'), ('30 seconds', '30'),
('1 minute', '60'), ('5 minutes', '300'), ('15 minutes', '900'),
('30 minutes', '1800'), ('1 hour', '3600'), ('2 hours', '7200'), ('3 hours', '10800')]
for text, value in intervals:
if value == str(new_interval):
self.interval_desc.set(text)
break
self.save_config()
self.log_message(f'Copy interval changed to {self.interval_desc.get()}')
except ValueError:
pass
def on_notify_change(self):
self.show_notifications = self.notify_var.get()
self.save_config()
def on_disable_log_change(self):
self.disable_logging = self.disable_log_var.get()
self.save_config()
def on_start_as_icon_change(self):
self.start_as_icon = self.start_as_icon_var.get()
self.save_config()
def on_auto_start_change(self):
self.auto_start = self.auto_start_var.get()
self.save_config()
def on_overwrite_change(self):
self.overwrite_files = self.overwrite_var.get()
self.save_config()
def start_timer(self):
if not self.copy_pairs:
messagebox.showwarning('Warning', 'Please add at least one source-destination pair')
return
self.timer_running = True
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.update_status('Running')
self.log_message('Automatic copy started')
self.copy_thread = threading.Thread(target=self.copy_worker, daemon=True)
self.copy_thread.start()
def stop_timer(self):
self.timer_running = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.update_status('Stopped')
self.log_message('Automatic copy stopped')
def copy_now(self):
if not self.copy_pairs:
messagebox.showwarning('Warning', 'Please add at least one source-destination pair')
return
threading.Thread(target=self.perform_copy, daemon=True).start()
def copy_worker(self):
while self.timer_running:
self.perform_copy()
for _ in range(self.interval):
if not self.timer_running:
return
time.sleep(1)
def perform_copy(self):
self.update_status('Copying...')
start_time = time.time()
total_files = 0
all_success = True
for source, dest in self.copy_pairs:
if not os.path.exists(source):
self.log_message(f'WARNING: Source folder does not exist: {source}')
all_success = False
continue
files_copied, success = self.copy_folder(source, dest, self.overwrite_files)
total_files += files_copied
if not success:
all_success = False
elapsed_time = time.time() - start_time
status_msg = f'Completed - {total_files} files in {elapsed_time:.1f}s'
self.update_status(status_msg)
if self.show_notifications and all_success:
self.show_notification('Copy Completed', f'Successfully copied {total_files} files')
def copy_folder(self, source, dest, overwrite=False):
files_copied = 0
operation_success = True
if not os.path.exists(dest):
os.makedirs(dest, exist_ok=True)
# Copy files one by one
for root, dirs, files in os.walk(source):
rel_path = os.path.relpath(root, source)
dest_dir = os.path.join(dest, rel_path)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir, exist_ok=True)
for file in files:
src_file = os.path.join(root, file)
dst_file = os.path.join(dest_dir, file)
try:
if overwrite or not os.path.exists(dst_file):
shutil.copy2(src_file, dst_file)
files_copied += 1
except Exception as e:
self.log_message(f'ERROR copying \'{src_file}\': {str(e)}')
operation_success = False
mode = "with overwrite" if overwrite else "missing files only"
self.log_message(f'Copied {files_copied} files {mode} from \'{os.path.basename(source)}\'')
return files_copied, operation_success
def clear_log(self):
self.log_text.config(state=tk.NORMAL)
self.log_text.delete('1.0', tk.END)
self.log_text.config(state=tk.DISABLED)
def show_notification(self, title, message):
try:
if self.tray_icon and self.is_minimized_to_tray:
self.tray_icon.notify(title, message)
else:
messagebox.showinfo(title, message)
except Exception as e:
self.log_message(f'Notification error: {str(e)}')
def minimize_to_tray(self):
self.root.withdraw()
self.is_minimized_to_tray = True
if not self.tray_icon:
image = self.create_tray_icon()
menu = pystray.Menu(
pystray.MenuItem('Show', self.show_from_tray),
pystray.MenuItem('Exit', self.exit_application)
)
self.tray_icon = pystray.Icon('Folder Copier', image, 'Folder Copier Tool', menu)
threading.Thread(target=self.tray_icon.run, daemon=True).start()
def create_tray_icon(self):
width = 64
height = 64
image = Image.new('RGB', (width, height), 'white')
dc = ImageDraw.Draw(image)
dc.rectangle([0, 0, width, height], fill='#1a73e8')
dc.rectangle([width//4, height//4, 3*width//4, 3*height//4], fill='white')
return image
def show_from_tray(self):
self.is_minimized_to_tray = False
self.root.deiconify()
def exit_application(self):
self.timer_running = False
self.saver_running = False
# Unregister all hotkeys
self.unregister_all_hotkeys()
if self.tray_icon:
self.tray_icon.stop()
self.root.quit()
self.root.destroy()
def create_auto_launch_shortcut(self):
try:
startup_folder = winshell.startup()
shortcut_path = os.path.join(startup_folder, 'FolderCopierTool.lnk')
target = sys.executable
wDir = os.path.dirname(os.path.abspath(__file__))
icon = sys.executable
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(shortcut_path)
shortcut.Targetpath = target
shortcut.Arguments = f'"{os.path.abspath(__file__)}"'
shortcut.WorkingDirectory = wDir
shortcut.IconLocation = icon
shortcut.save()
messagebox.showinfo('Success', 'Auto-launch shortcut created successfully!')
self.log_message('Auto-launch shortcut created in startup folder')
except Exception as e:
messagebox.showerror('Error', f'Failed to create auto-launch shortcut: {str(e)}')
self.log_message(f'ERROR creating auto-launch shortcut: {str(e)}')
def log_message(self, message):
if self.disable_logging:
return
def update_log():
self.log_text.config(state=tk.NORMAL)
timestamp = datetime.now().strftime('%H:%M:%S')
self.log_text.insert(tk.END, f'[{timestamp}] {message}\n')
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
self.root.after(0, update_log)
def update_status(self, message):
def update():
self.status_var.set(message)
self.root.after(0, update)
def load_config(self):
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r') as f:
config = json.load(f)
# Load copy configuration
pairs = config.get('copy_pairs', [])
self.copy_pairs = []
for source, dest in pairs:
try:
norm_source = os.path.normpath(source)
norm_dest = os.path.normpath(dest)
self.copy_pairs.append((norm_source, norm_dest))
except Exception:
pass
self.interval = config.get('interval', 5)
self.show_notifications = config.get('show_notifications', False)
self.disable_logging = config.get('disable_logging', False)
self.start_as_icon = config.get('start_as_icon', False)
self.auto_start = config.get('auto_start', True)
self.overwrite_files = config.get('overwrite_files', False)
# Load auto saver configuration
auto_pairs = config.get('auto_saver_pairs', [])
self.auto_saver_pairs = []
for source, dest in auto_pairs:
try:
norm_source = os.path.normpath(source)
norm_dest = os.path.normpath(dest)
self.auto_saver_pairs.append((norm_source, norm_dest))
except Exception:
pass
self.saver_hours = config.get('saver_hours', 0)
self.saver_minutes = config.get('saver_minutes', 0)
self.saver_seconds = config.get('saver_seconds', 30)
self.auto_start_saver = config.get('auto_start_saver', False)
# Load manual saver configuration
manual_pairs = config.get('manual_saver_pairs', [])
self.manual_saver_pairs = []
for item in manual_pairs:
try:
if len(item) == 3:
source, dest, hotkey = item
norm_source = os.path.normpath(source)
norm_dest = os.path.normpath(dest)
self.manual_saver_pairs.append((norm_source, norm_dest, hotkey))
except Exception:
pass
# Set variable values
self.disable_log_var.set(self.disable_logging)
self.start_as_icon_var.set(self.start_as_icon)
self.auto_start_var.set(self.auto_start)
self.notify_var.set(self.show_notifications)
self.overwrite_var.set(self.overwrite_files)
self.auto_start_saver_var.set(self.auto_start_saver)
except Exception as e:
self.log_message(f'Error loading configuration: {str(e)}')
self.use_default_config()
else:
self.use_default_config()
def use_default_config(self):
self.copy_pairs = []
self.interval = 5
self.show_notifications = False
self.disable_logging = False
self.start_as_icon = False
self.auto_start = True
self.overwrite_files = False
self.auto_saver_pairs = []
self.saver_hours = 0
self.saver_minutes = 0
self.saver_seconds = 30
self.auto_start_saver = False
self.manual_saver_pairs = []
self.disable_log_var.set(self.disable_logging)
self.start_as_icon_var.set(self.start_as_icon)
self.auto_start_var.set(self.auto_start)
self.notify_var.set(self.show_notifications)
self.overwrite_var.set(self.overwrite_files)
self.auto_start_saver_var.set(self.auto_start_saver)
def save_config(self):
try:
config = {
# Copy configuration
'copy_pairs': self.copy_pairs,
'interval': self.interval,
'show_notifications': self.show_notifications,
'disable_logging': self.disable_logging,
'start_as_icon': self.start_as_icon,
'auto_start': self.auto_start,
'overwrite_files': self.overwrite_files,
# Auto saver configuration
'auto_saver_pairs': self.auto_saver_pairs,
'saver_hours': self.saver_hours,
'saver_minutes': self.saver_minutes,
'saver_seconds': self.saver_seconds,
'auto_start_saver': self.auto_start_saver,
# Manual saver configuration
'manual_saver_pairs': self.manual_saver_pairs
}
with open(self.config_file, 'w') as f:
json.dump(config, f, indent=2)
except Exception as e:
self.log_message(f'ERROR: Cannot save configuration: {str(e)}')
def on_closing(self):
"""Handle window closing"""
if self.is_minimized_to_tray:
self.exit_application()
else:
result = messagebox.askyesnocancel('Exit',
'Do you want to minimize to system tray instead of exiting?\n\n'
'Yes: Minimize to tray\n'
'No: Exit completely\n'
'Cancel: Stay in application')
if result is True:
self.minimize_to_tray()
elif result is False:
self.exit_application()
# If result is None (Cancel), do nothing
if __name__ == '__main__':
root = tk.Tk()
app = FolderCopierTool(root)
root.mainloop()
Commenti
Posta un commento