- Improves the backup list display with chronological sorting, colored grouping for full/incremental backups, and dedicated time column. - Changes backup folder naming to a consistent dd-mm-yyyy_HH:MM:SS format. - Fixes bug where backup size was not displayed. - Adds detection for encrypted backup containers, showing them correctly in the list. - Hardens destination space check: - Considers extra space needed for compressed backups. - Disables start button if projected usage is > 95% or exceeds total disk space.
418 lines
18 KiB
Python
418 lines
18 KiB
Python
# pyimage/ui/drawing.py
|
|
import tkinter as tk
|
|
from pbp_app_config import AppConfig, Msg
|
|
import os
|
|
import threading
|
|
from shared_libs.animated_icon import AnimatedIcon
|
|
|
|
|
|
class Drawing:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
def _start_calculating_animation(self, canvas):
|
|
# Stop animation for the specific canvas
|
|
self._stop_calculating_animation(canvas)
|
|
animation_type = self.app.config_manager.get_setting(
|
|
"calculation_animation_type", "double_arc")
|
|
|
|
if canvas == self.app.left_canvas:
|
|
self.app.left_canvas_animation = AnimatedIcon(
|
|
canvas, width=20, height=20, animation_type=animation_type, use_pillow=True)
|
|
self.app.left_canvas_animation.start()
|
|
else: # right_canvas
|
|
self.app.right_canvas_animation = AnimatedIcon(
|
|
canvas, width=20, height=20, animation_type=animation_type, use_pillow=True)
|
|
self.app.right_canvas_animation.start()
|
|
|
|
def _stop_calculating_animation(self, canvas=None):
|
|
if canvas is None: # Stop all
|
|
if self.app.left_canvas_animation:
|
|
self.app.left_canvas_animation.stop()
|
|
self.app.left_canvas_animation.destroy()
|
|
self.app.left_canvas_animation = None
|
|
if self.app.right_canvas_animation:
|
|
self.app.right_canvas_animation.stop()
|
|
self.app.right_canvas_animation.destroy()
|
|
self.app.right_canvas_animation = None
|
|
return
|
|
|
|
animation_to_stop = None
|
|
if canvas == self.app.left_canvas:
|
|
animation_to_stop = self.app.left_canvas_animation
|
|
self.app.left_canvas_animation = None
|
|
else:
|
|
animation_to_stop = self.app.right_canvas_animation
|
|
self.app.right_canvas_animation = None
|
|
|
|
if animation_to_stop:
|
|
animation_to_stop.stop()
|
|
animation_to_stop.destroy()
|
|
|
|
def redraw_left_canvas(self, event=None):
|
|
canvas = self.app.left_canvas
|
|
canvas.delete("all")
|
|
width = canvas.winfo_width()
|
|
height = canvas.winfo_height()
|
|
|
|
left_canvas_title = Msg.STR["source"] if self.app.mode == "backup" else Msg.STR["destination"]
|
|
canvas.create_text(10, 10, anchor="nw", text=left_canvas_title, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 12, "bold"))
|
|
|
|
icon_name = self.app.left_canvas_data.get('icon')
|
|
if icon_name:
|
|
icon = self.app.image_manager.get_icon(icon_name)
|
|
if icon:
|
|
canvas.create_image(width / 2, 60, image=icon)
|
|
|
|
folder_name = self.app.left_canvas_data.get('folder', '')
|
|
canvas.create_text(width / 2, 120, text=folder_name, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 14, "bold"))
|
|
|
|
size_text = self.app.left_canvas_data.get('size', '')
|
|
if size_text:
|
|
canvas.create_text(width / 2, 140, text=size_text)
|
|
|
|
extra_info = self.app.left_canvas_data.get('extra_info', '')
|
|
if extra_info:
|
|
canvas.create_text(width / 2, 160, text=extra_info, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 12, "bold"), fill="black")
|
|
|
|
path_display = self.app.left_canvas_data.get('path_display', '')
|
|
if path_display:
|
|
canvas.create_text(width / 2, 180, text=path_display, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 10), fill="gray")
|
|
|
|
if self.app.left_canvas_data.get('calculating', False):
|
|
if self.app.left_canvas_animation:
|
|
self.app.left_canvas_animation.place(
|
|
x=width/2 + 70, y=140, anchor="center")
|
|
|
|
def redraw_right_canvas(self, event=None):
|
|
"""Dispatches to the correct drawing method based on the current mode."""
|
|
if self.app.mode == "backup":
|
|
self.redraw_right_canvas_backup(event)
|
|
else: # "restore"
|
|
self.redraw_right_canvas_restore(event)
|
|
|
|
def redraw_right_canvas_backup(self, event=None):
|
|
canvas = self.app.right_canvas
|
|
canvas.delete("all")
|
|
width = canvas.winfo_width()
|
|
height = canvas.winfo_height()
|
|
|
|
right_canvas_title = Msg.STR["destination"]
|
|
canvas.create_text(10, 10, anchor="nw", text=right_canvas_title, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 12, "bold"))
|
|
|
|
icon_name = 'hdd_extralarge'
|
|
if icon_name:
|
|
icon = self.app.image_manager.get_icon(icon_name)
|
|
if icon:
|
|
canvas.create_image(width / 2, 60, image=icon)
|
|
|
|
folder_name = self.app.right_canvas_data.get('folder', '')
|
|
canvas.create_text(width / 2, 120, text=folder_name, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 14, "bold"))
|
|
|
|
size_text = self.app.right_canvas_data.get('size', '')
|
|
if size_text:
|
|
canvas.create_text(width / 2, 140, text=size_text)
|
|
|
|
path_display = self.app.right_canvas_data.get('path_display', '')
|
|
if path_display:
|
|
canvas.create_text(width / 2, 160, text=path_display, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 10), fill="gray")
|
|
|
|
def redraw_right_canvas_restore(self, event=None):
|
|
canvas = self.app.right_canvas
|
|
canvas.delete("all")
|
|
width = canvas.winfo_width()
|
|
height = canvas.winfo_height()
|
|
|
|
right_canvas_title = Msg.STR["source"]
|
|
canvas.create_text(10, 10, anchor="nw", text=right_canvas_title, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 12, "bold"))
|
|
|
|
icon_name = 'hdd_extralarge'
|
|
if icon_name:
|
|
icon = self.app.image_manager.get_icon(icon_name)
|
|
if icon:
|
|
canvas.create_image(width / 2, 60, image=icon)
|
|
|
|
folder_name = self.app.right_canvas_data.get('folder', '')
|
|
canvas.create_text(width / 2, 120, text=folder_name, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 14, "bold"))
|
|
|
|
size_text = self.app.right_canvas_data.get('size', '')
|
|
if size_text:
|
|
canvas.create_text(width / 2, 140, text=size_text)
|
|
|
|
path_display = self.app.right_canvas_data.get('path_display', '')
|
|
if path_display:
|
|
canvas.create_text(width / 2, 160, text=path_display, font=(
|
|
AppConfig.UI_CONFIG["font_family"], 10), fill="gray")
|
|
|
|
if self.app.right_canvas_data.get('calculating', False):
|
|
if self.app.right_canvas_animation:
|
|
self.app.right_canvas_animation.place(
|
|
x=width/2 + 70, y=140, anchor="center")
|
|
|
|
def start_backup_calculation_display(self):
|
|
self._start_calculating_animation(self.app.left_canvas)
|
|
self.redraw_left_canvas()
|
|
|
|
def calculate_restore_folder_size(self):
|
|
if self.app.right_calculation_thread and self.app.right_calculation_thread.is_alive():
|
|
self.app.right_calculation_stop_event.set()
|
|
|
|
self.app.start_pause_button.config(state="disabled")
|
|
path_to_calculate = self.app.right_canvas_data.get('path_display')
|
|
if path_to_calculate and os.path.isdir(path_to_calculate):
|
|
self.app.right_canvas_data['calculating'] = True
|
|
self.app.right_canvas_data['size'] = Msg.STR['calculating_size']
|
|
self._start_calculating_animation(self.app.right_canvas)
|
|
self.redraw_right_canvas_restore()
|
|
|
|
self.app.right_calculation_stop_event = threading.Event()
|
|
self.app.right_calculation_thread = threading.Thread(target=self._calculate_and_update_restore_size, args=(
|
|
path_to_calculate, self.app.right_calculation_stop_event), daemon=True)
|
|
self.app.right_calculation_thread.start()
|
|
|
|
def _calculate_and_update_restore_size(self, path, stop_event):
|
|
total_size = 0
|
|
try:
|
|
for dirpath, dirnames, filenames in os.walk(path):
|
|
if stop_event.is_set():
|
|
return
|
|
for f in filenames:
|
|
if stop_event.is_set():
|
|
return
|
|
fp = os.path.join(dirpath, f)
|
|
if not os.path.islink(fp):
|
|
try:
|
|
total_size += os.path.getsize(fp)
|
|
except OSError:
|
|
pass # Ignore files that can't be accessed
|
|
except OSError as e:
|
|
print(f"Error calculating size for {path}: {e}")
|
|
size_text = "Error"
|
|
else:
|
|
if stop_event.is_set():
|
|
return
|
|
size_gb = total_size / (1024**3)
|
|
size_text = f"{size_gb:.2f} GB"
|
|
self.app.restore_source_size_bytes = total_size
|
|
|
|
def update_ui():
|
|
if stop_event.is_set():
|
|
return
|
|
self.app.right_canvas_data['size'] = size_text
|
|
self.app.right_canvas_data['calculating'] = False
|
|
self._stop_calculating_animation(self.app.right_canvas)
|
|
self.redraw_right_canvas_restore()
|
|
|
|
if not self.app.left_canvas_data.get('calculating', False):
|
|
self.app.start_pause_button.config(state="normal")
|
|
|
|
if not stop_event.is_set():
|
|
self.app.after(0, update_ui)
|
|
|
|
def update_target_projection(self):
|
|
if self.app.mode == "restore":
|
|
self.update_restore_projection_before()
|
|
self.update_restore_projection_after()
|
|
return
|
|
|
|
if self.app.destination_total_bytes == 0:
|
|
return
|
|
|
|
canvas = self.app.target_size_canvas
|
|
canvas.delete("all")
|
|
canvas_width = canvas.winfo_width()
|
|
if canvas_width <= 1:
|
|
self.app.after(50, self.update_target_projection)
|
|
return
|
|
|
|
# Determine required space, considering compression
|
|
required_space = self.app.source_size_bytes
|
|
if self.app.compressed_var.get():
|
|
required_space *= 2 # Double the space for compression process
|
|
|
|
projected_total_used = self.app.destination_used_bytes + required_space
|
|
|
|
if self.app.destination_total_bytes > 0:
|
|
projected_total_percentage = projected_total_used / self.app.destination_total_bytes
|
|
else:
|
|
projected_total_percentage = 0
|
|
|
|
info_font = (AppConfig.UI_CONFIG["font_family"], 10, "bold")
|
|
info_messages = []
|
|
|
|
# First, check for critical space issues
|
|
if projected_total_used > self.app.destination_total_bytes or projected_total_percentage >= 0.95:
|
|
self.app.start_pause_button.config(state="disabled")
|
|
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(), fill="#D32F2F", outline="") # Red bar
|
|
info_messages.append(Msg.STR["warning_not_enough_space"])
|
|
|
|
elif projected_total_percentage >= 0.90:
|
|
self.app.start_pause_button.config(state="normal")
|
|
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(), fill="#E8740C", outline="") # Orange bar
|
|
info_messages.append(Msg.STR["warning_space_over_90_percent"])
|
|
|
|
else:
|
|
# Only enable the button if the source is not larger than the partition itself
|
|
if not self.app.source_larger_than_partition:
|
|
self.app.start_pause_button.config(state="normal")
|
|
else:
|
|
self.app.start_pause_button.config(state="disabled")
|
|
|
|
used_percentage = self.app.destination_used_bytes / self.app.destination_total_bytes
|
|
used_width = canvas_width * used_percentage
|
|
canvas.create_rectangle(0, 0, used_width, canvas.winfo_height(), fill="#0078d7", outline="")
|
|
|
|
# Draw the projected part only if there is space
|
|
projected_percentage = self.app.source_size_bytes / self.app.destination_total_bytes
|
|
projected_width = canvas_width * projected_percentage
|
|
canvas.create_rectangle(used_width, 0, used_width + projected_width, canvas.winfo_height(), fill="#50E6FF", outline="")
|
|
|
|
# Add other informational messages if no critical warnings are present
|
|
if not info_messages:
|
|
if self.app.source_larger_than_partition:
|
|
info_messages.append(Msg.STR["warning_source_larger_than_partition"])
|
|
elif self.app.is_first_backup:
|
|
info_messages.append(Msg.STR["ready_for_first_backup"])
|
|
elif self.app.mode == "backup":
|
|
info_messages.append(Msg.STR["backup_mode_info"])
|
|
else:
|
|
info_messages.append(Msg.STR["restore_mode_info"])
|
|
|
|
self.app.info_label.config(text="\n".join(info_messages), font=info_font)
|
|
|
|
self.app.target_size_label.config(
|
|
text=f"{projected_total_used / (1024**3):.2f} GB / {self.app.destination_total_bytes / (1024**3):.2f} GB")
|
|
|
|
def update_restore_projection_before(self):
|
|
if self.app.destination_total_bytes == 0:
|
|
return
|
|
|
|
canvas = self.app.restore_size_canvas_before
|
|
canvas.delete("all")
|
|
canvas_width = canvas.winfo_width()
|
|
if canvas_width <= 1:
|
|
self.app.after(50, self.update_restore_projection_before)
|
|
return
|
|
|
|
used_percentage = self.app.destination_used_bytes / \
|
|
self.app.destination_total_bytes
|
|
used_width = canvas_width * used_percentage
|
|
canvas.create_rectangle(
|
|
0, 0, used_width, canvas.winfo_height(), fill="#0078d7", outline="")
|
|
|
|
self.app.restore_size_label_before.config(
|
|
text=f"{self.app.destination_used_bytes / (1024**3):.2f} GB / {self.app.destination_total_bytes / (1024**3):.2f} GB")
|
|
|
|
def update_restore_projection_after(self):
|
|
if self.app.destination_total_bytes == 0:
|
|
return
|
|
|
|
canvas = self.app.restore_size_canvas_after
|
|
canvas.delete("all")
|
|
canvas_width = canvas.winfo_width()
|
|
if canvas_width <= 1:
|
|
self.app.after(50, self.update_restore_projection_after)
|
|
return
|
|
|
|
destination_folder_bytes = getattr(
|
|
self.app, 'restore_destination_folder_size_bytes', 0)
|
|
source_bytes = getattr(self.app, 'restore_source_size_bytes', 0)
|
|
|
|
size_diff_bytes = source_bytes - destination_folder_bytes
|
|
projected_total_used = self.app.destination_used_bytes + size_diff_bytes
|
|
|
|
if self.app.destination_total_bytes > 0:
|
|
projected_total_percentage = projected_total_used / \
|
|
self.app.destination_total_bytes
|
|
else:
|
|
projected_total_percentage = 0
|
|
|
|
if projected_total_percentage >= 0.95:
|
|
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(),
|
|
fill="#ff0000", outline="") # Red bar
|
|
elif projected_total_percentage >= 0.90:
|
|
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(),
|
|
fill="#ff8c00", outline="") # Orange bar
|
|
else:
|
|
if self.app.destination_total_bytes > 0:
|
|
used_percentage = self.app.destination_used_bytes / \
|
|
self.app.destination_total_bytes
|
|
used_width = canvas_width * used_percentage
|
|
canvas.create_rectangle(
|
|
0, 0, used_width, canvas.winfo_height(), fill="#0078d7", outline="")
|
|
|
|
if size_diff_bytes > 0:
|
|
projected_percentage = size_diff_bytes / self.app.destination_total_bytes
|
|
projected_width = canvas_width * projected_percentage
|
|
canvas.create_rectangle(used_width, 0, used_width + projected_width,
|
|
canvas.winfo_height(), fill="#ff8c00", outline="")
|
|
elif size_diff_bytes < 0:
|
|
final_used_width = canvas_width * \
|
|
(projected_total_used / self.app.destination_total_bytes)
|
|
canvas.delete("all")
|
|
canvas.create_rectangle(
|
|
0, 0, final_used_width, canvas.winfo_height(), fill="#0078d7", outline="")
|
|
|
|
size_diff_gb = size_diff_bytes / (1024**3)
|
|
diff_text = f"+{size_diff_gb:.2f} GB" if size_diff_gb >= 0 else f"{size_diff_gb:.2f} GB"
|
|
|
|
diff_color = "blue"
|
|
|
|
self.app.restore_size_label_diff.config(
|
|
text=diff_text, foreground=diff_color)
|
|
|
|
self.app.restore_size_label_after.config(
|
|
text=f"{projected_total_used / (1024**3):.2f} GB / {self.app.destination_total_bytes / (1024**3):.2f} GB")
|
|
|
|
def update_nav_buttons(self, active_index):
|
|
for i, button in enumerate(self.app.nav_buttons):
|
|
if i == active_index:
|
|
button.configure(style="Toolbutton")
|
|
self.app.nav_progress_bars[i].pack(side=tk.BOTTOM, fill=tk.X)
|
|
self.app.nav_progress_bars[i]['value'] = 100
|
|
else:
|
|
button.configure(style="Gray.Toolbutton")
|
|
self.app.nav_progress_bars[i].pack_forget()
|
|
|
|
def reset_projection_canvases(self):
|
|
# Reset data variables related to size calculations
|
|
self.app.source_size_bytes = 0
|
|
self.app.restore_source_size_bytes = 0
|
|
self.app.restore_destination_folder_size_bytes = 0
|
|
self.app.source_larger_than_partition = False
|
|
|
|
# Clear backup mode canvases and labels
|
|
self.app.source_size_canvas.delete("all")
|
|
self.app.target_size_canvas.delete("all")
|
|
self.app.source_size_label.config(text="0.00 GB / 0.00 GB")
|
|
self.app.target_size_label.config(text="0.00 GB / 0.00 GB")
|
|
|
|
# Clear restore mode canvases and labels
|
|
self.app.restore_size_canvas_before.delete("all")
|
|
self.app.restore_size_canvas_after.delete("all")
|
|
self.app.restore_size_label_before.config(text="0.00 GB / 0.00 GB")
|
|
self.app.restore_size_label_after.config(text="0.00 GB / 0.00 GB")
|
|
if hasattr(self.app, 'restore_size_label_diff'):
|
|
self.app.restore_size_label_diff.config(text="")
|
|
|
|
# Also reset the main info label
|
|
self.app.info_label.config(text="")
|
|
|
|
def reset_all_canvases(self):
|
|
self.reset_projection_canvases()
|
|
self.app.left_canvas.delete("all")
|
|
self.app.right_canvas.delete("all")
|
|
self.app.left_canvas_data.clear()
|
|
self.app.right_canvas_data.clear()
|
|
self.redraw_left_canvas()
|
|
self.redraw_right_canvas()
|