fix(ui, calculation, state-management): Improve disk usage display and fix race

conditions

This commit addresses several issues related to the disk usage display in both
backup and restore modes, and resolves race conditions during mode switches.

**Corrected restore mode projection calculation:**
Introduced dedicated state variables (`restore_destination_folder_size_bytes`,
`restore_source_size_bytes`) to accurately calculate and display projected disk
usage and size differences in restore mode, preventing state pollution from other
modes.

**Ensured correct disk display on mode switch:**
Implemented logic to re-evaluate and set `destination_total_bytes` and
`destination_used_bytes` based on the currently active mode's selected path. This
prevents stale disk information from one mode being displayed in another.

**Fixed race conditions during background calculations:**
Implemented a robust cancellation mechanism for all background folder size
calculations. Threads now pass their originating mode, and results are discarded if
the application's mode has changed, preventing UI state corruption. This includes
managing both left and right canvas calculation threads.

**feat(ui): Added explicit UI reset on action:**
Introduced a `reset_projection_canvases` function to clear all projection
displays and reset relevant data variables immediately when a new action (e.g.,
folder selection, mode switch) is initiated. This provides a cleaner user
experience by preventing stale data from being shown while new calculations are in
progress.

**Resolved missing imports:**
Added necessary `import os` and `import shutil` statements to
`pyimage_ui/navigation.py` to resolve runtime errors.
This commit is contained in:
2025-08-27 00:57:59 +02:00
parent 2f7992777d
commit 8522168ec5
5 changed files with 58 additions and 6 deletions

View File

@@ -84,6 +84,8 @@ class MainApplication(tk.Tk):
self.left_canvas_animation = None
self.right_canvas_animation = None
self.right_calculation_thread = None
self.right_calculation_stop_event = None
self.destination_path = None
self.source_size_bytes = 0
self.destination_used_bytes = 0

View File

@@ -160,6 +160,9 @@ class Drawing:
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):
@@ -167,35 +170,50 @@ class Drawing:
self.app.right_canvas_data['size'] = Msg.STR['calculating_size']
self._start_calculating_animation(self.app.right_canvas)
self.redraw_right_canvas_restore()
threading.Thread(target=self._calculate_and_update_restore_size, args=(path_to_calculate,), daemon=True).start()
def _calculate_and_update_restore_size(self, path):
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):
total_size += os.path.getsize(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()
# Only enable the button if the OTHER calculation is not running.
if not self.app.left_canvas_data.get('calculating', False):
self.app.start_pause_button.config(state="normal")
self.app.after(0, update_ui)
if not stop_event.is_set():
self.app.after(0, update_ui)
def update_target_projection(self):
if self.app.mode == "restore":

View File

@@ -1,4 +1,6 @@
# pyimage_ui/navigation.py
import os
import shutil
from shared_libs.message import MessageDialog
from app_config import Msg
@@ -10,6 +12,8 @@ class Navigation:
def _cancel_calculation(self):
if self.app.calculation_thread and self.app.calculation_thread.is_alive():
self.app.calculation_stop_event.set()
if self.app.right_calculation_thread and self.app.right_calculation_thread.is_alive():
self.app.right_calculation_stop_event.set()
# Stop all calculation animations and update UI
self.app.drawing._stop_calculating_animation() # Stops both left and right
@@ -25,16 +29,44 @@ class Navigation:
self.app.mode = mode
self._cancel_calculation()
# Point to the correct data dictionaries for the mode
if mode == "backup":
self.app.left_canvas_data = self.app.backup_left_canvas_data
self.app.right_canvas_data = self.app.backup_right_canvas_data
active_index = 0
# Restore backup destination disk metrics
backup_dest_path = self.app.backup_right_canvas_data.get('path_display')
if backup_dest_path and os.path.isdir(backup_dest_path):
try:
total, used, free = shutil.disk_usage(backup_dest_path)
self.app.destination_total_bytes = total
self.app.destination_used_bytes = used
except FileNotFoundError:
self.app.destination_total_bytes = 0
self.app.destination_used_bytes = 0
else:
self.app.destination_total_bytes = 0
self.app.destination_used_bytes = 0
else: # restore
self.app.left_canvas_data = self.app.restore_left_canvas_data
self.app.right_canvas_data = self.app.restore_right_canvas_data
active_index = 1
# Restore restore destination disk metrics
restore_dest_path = self.app.restore_left_canvas_data.get('path_display')
if restore_dest_path and os.path.isdir(restore_dest_path):
try:
total, used, free = shutil.disk_usage(restore_dest_path)
self.app.destination_total_bytes = total
self.app.destination_used_bytes = used
except FileNotFoundError:
self.app.destination_total_bytes = 0
self.app.destination_used_bytes = 0
else:
self.app.destination_total_bytes = 0
self.app.destination_used_bytes = 0
# Set default content if the dictionaries are empty
if not self.app.left_canvas_data:
if mode == "backup":