430 lines
17 KiB
Python
430 lines
17 KiB
Python
"""
|
|
Dialog Manager Module
|
|
Handles various dialog boxes and message windows for the application.
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import messagebox, filedialog, scrolledtext
|
|
import os
|
|
from typing import Optional, Tuple
|
|
|
|
|
|
class DialogManager:
|
|
"""Manages various dialog boxes and user interactions."""
|
|
|
|
def __init__(self, parent_window: tk.Tk):
|
|
"""Initialize the dialog manager.
|
|
|
|
Args:
|
|
parent_window: The main window instance
|
|
"""
|
|
self.parent = parent_window
|
|
self.troubleshooting_steps = (
|
|
"Android device appears to have been disconnected and/or USB debugging is disabled.\n"
|
|
"Please ensure your Android device is securely connected at both ends.\n\n"
|
|
|
|
"To enable USB debugging:\n"
|
|
"1. Connect your device to the computer via USB\n"
|
|
"2. Open Settings → About phone\n"
|
|
"3. Tap 'Build number' seven times to unlock Developer Options\n"
|
|
" (You only need to do this once unless you disable it, reset settings, or wipe your device)\n"
|
|
"4. Navigate back and go to System → Developer Options\n"
|
|
"5. Find and enable 'USB debugging'\n"
|
|
" (Tip: Use the search icon at the top if you can't find it)\n"
|
|
"6. Connect via USB and tap 'Trust' when prompted\n"
|
|
" (Checking 'Remember' is recommended for future transfers)\n\n"
|
|
|
|
"Ensure File Transfer mode is enabled:\n"
|
|
"1. After connecting, swipe down to view notifications\n"
|
|
"2. Look for a USB notification (often shows 'Charging over USB')\n"
|
|
"3. Tap the notification and select 'File Transfer' or 'MTP' mode\n\n"
|
|
|
|
"Note: Menu names may vary by Android version:\n"
|
|
"• Some devices show 'Developer options' under 'System'\n"
|
|
"• Others may have it directly in the main Settings menu\n"
|
|
"• Samsung devices might show 'Software information' instead of 'About phone'\n\n"
|
|
|
|
"If you're still having trouble:\n"
|
|
"• Try a different USB cable or port (some cables only support charging)\n"
|
|
"• Restart both your phone and computer\n"
|
|
"• Make sure your phone screen is unlocked when connecting\n"
|
|
"• Set your phone screen timeout to 30 minutes (especially for long transfers)\n\n"
|
|
"• Use a different computer to test if the issue is computer-specific\n"
|
|
|
|
"Windows users: If you see a driver installation popup, please allow it to complete.\n"
|
|
"Linux users: You may need to run 'sudo usermod -a -G plugdev $USER' and reboot.\n"
|
|
|
|
"After completing these steps, click 'Recheck for connected Android device' to try again."
|
|
)
|
|
|
|
def show_file_folder_selection_notice(self) -> bool:
|
|
"""Show instructions for file and folder selection in a custom dialog.
|
|
|
|
Returns:
|
|
True if user clicked OK, False if user cancelled or closed dialog
|
|
"""
|
|
# Create custom dialog window
|
|
dialog = tk.Toplevel(self.parent)
|
|
dialog.title("File/Folder Selection Notice")
|
|
dialog.geometry("600x250")
|
|
dialog.minsize(600, 250)
|
|
dialog.resizable(True, True)
|
|
dialog.transient(self.parent)
|
|
dialog.grab_set()
|
|
|
|
# Center the dialog on the parent window
|
|
dialog.update_idletasks()
|
|
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (600 // 2)
|
|
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (250 // 2)
|
|
dialog.geometry(f"600x250+{x}+{y}")
|
|
|
|
# Track if OK was clicked
|
|
dialog_confirmed = False
|
|
|
|
# Create main frame
|
|
main_frame = tk.Frame(dialog)
|
|
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
|
|
|
# Selection notice text
|
|
notice_text = (
|
|
"How file and folder selection works in this application\n\n"
|
|
"If you want to select a file for transfer, simply click on the file to select it.\n\n"
|
|
"If you want to select a folder for transfer, the folder that you navigate to "
|
|
"(the current directory you are viewing, not a highlighted folder) will be selected for transfer.\n\n"
|
|
"You can only transfer one file or one folder at a time."
|
|
)
|
|
|
|
# Create responsive text label
|
|
text_label = tk.Label(
|
|
main_frame,
|
|
text=notice_text,
|
|
justify="left",
|
|
anchor="nw",
|
|
wraplength=0, # Will be set dynamically
|
|
font=("Arial", 10)
|
|
)
|
|
text_label.pack(fill="both", expand=True, pady=(0, 20))
|
|
|
|
# OK button with confirmation callback
|
|
def on_ok_clicked():
|
|
nonlocal dialog_confirmed
|
|
dialog_confirmed = True
|
|
dialog.destroy()
|
|
|
|
ok_button = tk.Button(
|
|
main_frame,
|
|
text="OK",
|
|
command=on_ok_clicked,
|
|
width=10,
|
|
font=("Arial", 10)
|
|
)
|
|
ok_button.pack(pady=10)
|
|
|
|
# Configure text wrapping on dialog resize
|
|
def on_dialog_configure(event):
|
|
if event.widget == dialog:
|
|
# Calculate available width for text (account for padding and margins)
|
|
available_width = dialog.winfo_width() - 60 # 20px padding * 2 + some margin
|
|
if available_width > 200: # Minimum reasonable width
|
|
text_label.config(wraplength=available_width)
|
|
|
|
dialog.bind("<Configure>", on_dialog_configure)
|
|
|
|
# Set initial wrap length
|
|
dialog.after(10, lambda: on_dialog_configure(type('Event', (), {'widget': dialog})()))
|
|
|
|
# Handle window close (X button) - treat as cancel
|
|
def on_dialog_close():
|
|
nonlocal dialog_confirmed
|
|
dialog_confirmed = False
|
|
dialog.destroy()
|
|
|
|
dialog.protocol("WM_DELETE_WINDOW", on_dialog_close)
|
|
|
|
# Wait for dialog to close
|
|
dialog.wait_window()
|
|
|
|
return dialog_confirmed
|
|
|
|
def show_enable_debugging_instructions(self, callback: Optional[callable] = None) -> None:
|
|
"""Show instructions to connect device, enable file transfer, and enable USB debugging.
|
|
|
|
Args:
|
|
callback: Optional callback to execute after dialog is closed
|
|
"""
|
|
# Create custom dialog window
|
|
dialog = tk.Toplevel(self.parent)
|
|
dialog.title("Check device connection and enable USB Debugging")
|
|
dialog.geometry("700x800") # Increased height for better button spacing
|
|
dialog.minsize(700, 800) # Set minimum size to ensure all content is visible
|
|
dialog.resizable(True, True)
|
|
dialog.transient(self.parent)
|
|
dialog.grab_set()
|
|
|
|
# Center the dialog on the parent window
|
|
dialog.update_idletasks()
|
|
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (700 // 2)
|
|
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (450 // 2)
|
|
dialog.geometry(f"700x450+{x}+{y}")
|
|
|
|
# Create main frame
|
|
main_frame = tk.Frame(dialog)
|
|
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
|
|
|
# Create responsive text label
|
|
text_label = tk.Label(
|
|
main_frame,
|
|
text=self.troubleshooting_steps,
|
|
justify="left",
|
|
anchor="nw",
|
|
wraplength=0, # Will be set dynamically
|
|
font=("Arial", 10)
|
|
)
|
|
text_label.pack(fill="both", expand=True, pady=(0, 20))
|
|
|
|
# OK button
|
|
ok_button = tk.Button(
|
|
main_frame,
|
|
text="OK",
|
|
command=dialog.destroy,
|
|
width=10,
|
|
font=("Arial", 10)
|
|
)
|
|
ok_button.pack(pady=10)
|
|
|
|
# Configure text wrapping on dialog resize
|
|
def on_dialog_configure(event):
|
|
if event.widget == dialog:
|
|
# Calculate available width for text (account for padding and margins)
|
|
available_width = dialog.winfo_width() - 60 # 20px padding * 2 + some margin
|
|
if available_width > 200: # Minimum reasonable width
|
|
text_label.config(wraplength=available_width)
|
|
|
|
dialog.bind("<Configure>", on_dialog_configure)
|
|
|
|
# Set initial wrap length
|
|
dialog.after(10, lambda: on_dialog_configure(type('Event', (), {'widget': dialog})()))
|
|
|
|
# Handle dialog close
|
|
def on_close():
|
|
dialog.destroy()
|
|
if callback:
|
|
callback()
|
|
|
|
# After user clicks OK, ensure callback is executed
|
|
dialog.protocol("WM_DELETE_WINDOW", on_close)
|
|
ok_button.config(command=on_close)
|
|
|
|
def show_disable_debugging_reminder(self) -> None:
|
|
"""Show reminder to disable USB debugging after transfer."""
|
|
msg = (
|
|
"Transfer completed!\n\n"
|
|
"For security, please disable USB debugging when done:\n"
|
|
"Settings → Developer Options → disable 'USB debugging'."
|
|
)
|
|
messagebox.showinfo("Disable USB Debugging", msg)
|
|
|
|
def show_transfer_stats(self, stats: dict, operation: str, deduplicator=None) -> None:
|
|
"""Show transfer statistics dialog.
|
|
|
|
Args:
|
|
stats: Dictionary containing transfer statistics
|
|
operation: Type of operation ("Pull" or "Push")
|
|
deduplicator: Optional deduplicator instance for byte formatting
|
|
"""
|
|
# Format bytes saved
|
|
bytes_saved_str = ""
|
|
if stats['bytes_saved'] > 0:
|
|
if deduplicator and hasattr(deduplicator, 'format_bytes'):
|
|
bytes_saved_str = f" ({deduplicator.format_bytes(stats['bytes_saved'])} saved)"
|
|
else:
|
|
bytes_saved_str = f" ({stats['bytes_saved']} bytes saved)"
|
|
|
|
# Build message
|
|
title = f"{operation} Transfer Complete"
|
|
|
|
if stats['total_files'] == 0:
|
|
message = "No files were found to transfer."
|
|
else:
|
|
message_parts = [
|
|
f"Transfer completed successfully!\n",
|
|
f"Files found: {stats['total_files']}",
|
|
f"Files transferred: {stats['transferred']}",
|
|
f"Duplicate files skipped: {stats['skipped']}{bytes_saved_str}"
|
|
]
|
|
|
|
if stats['skipped'] > 0:
|
|
message_parts.append(f"\nDuplicate detection helped avoid unnecessary transfers!")
|
|
|
|
message = "\n".join(message_parts)
|
|
|
|
# Show dialog
|
|
messagebox.showinfo(title, message)
|
|
|
|
def show_error(self, title: str, message: str) -> None:
|
|
"""Show an error dialog.
|
|
|
|
Args:
|
|
title: Dialog title
|
|
message: Error message to display
|
|
"""
|
|
messagebox.showerror(title, message)
|
|
|
|
def show_info(self, title: str, message: str) -> None:
|
|
"""Show an info dialog.
|
|
|
|
Args:
|
|
title: Dialog title
|
|
message: Info message to display
|
|
"""
|
|
messagebox.showinfo(title, message)
|
|
|
|
def browse_local_file_or_folder(self, direction: str, initial_dir: Optional[str] = None) -> Optional[str]:
|
|
"""Browse for local file or folder based on transfer direction.
|
|
|
|
Args:
|
|
direction: Transfer direction ('pull' or 'push')
|
|
initial_dir: Initial directory to open browser in
|
|
|
|
Returns:
|
|
Selected path or None if cancelled
|
|
"""
|
|
if initial_dir is None:
|
|
initial_dir = os.path.expanduser("~")
|
|
|
|
# Show helpful notification about folder selection behavior
|
|
if not self.show_file_folder_selection_notice():
|
|
return None # User cancelled the notice dialog
|
|
|
|
if direction == "push":
|
|
# For push, show file selection first, then folder selection if cancelled
|
|
|
|
# First try file selection
|
|
selected_path = filedialog.askopenfilename(
|
|
title="Select file to push to Android device",
|
|
initialdir=initial_dir,
|
|
filetypes=[("All files", "*.*")]
|
|
)
|
|
|
|
# If no file was selected, offer folder selection as an alternative
|
|
if not selected_path:
|
|
selected_path = filedialog.askdirectory(
|
|
title="Select folder to push to Android device",
|
|
initialdir=initial_dir
|
|
)
|
|
|
|
return selected_path if selected_path else None
|
|
|
|
else: # pull direction
|
|
# For pull, only allow folder selection (destination)
|
|
folder = filedialog.askdirectory(
|
|
title="Select destination folder for pulled files",
|
|
initialdir=initial_dir
|
|
)
|
|
return folder if folder else None
|
|
|
|
def show_file_folder_choice(self, on_file_callback, on_folder_callback):
|
|
"""Show a dialog to choose between file or folder selection.
|
|
|
|
Args:
|
|
on_file_callback: Callback for file selection
|
|
on_folder_callback: Callback for folder selection
|
|
"""
|
|
# Ask user whether they want to select a file or folder
|
|
choice_window = tk.Toplevel(self.parent)
|
|
choice_window.title("Select File or Folder")
|
|
choice_window.geometry("350x150")
|
|
choice_window.resizable(False, False)
|
|
choice_window.transient(self.parent)
|
|
choice_window.grab_set()
|
|
|
|
# Center the window
|
|
choice_window.geometry("+{}+{}".format(
|
|
self.parent.winfo_x() + 100,
|
|
self.parent.winfo_y() + 100
|
|
))
|
|
|
|
main_frame = tk.Frame(choice_window)
|
|
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
|
|
|
tk.Label(main_frame, text="What would you like to select?",
|
|
font=("Arial", 11)).pack(pady=(0, 15))
|
|
|
|
button_frame = tk.Frame(main_frame)
|
|
button_frame.pack(fill="x")
|
|
|
|
def select_file():
|
|
choice_window.destroy()
|
|
on_file_callback()
|
|
|
|
def select_folder():
|
|
choice_window.destroy()
|
|
on_folder_callback()
|
|
|
|
def cancel_selection():
|
|
choice_window.destroy()
|
|
|
|
tk.Button(button_frame, text="📄 File", command=select_file,
|
|
width=12, font=("Arial", 10)).pack(side="left", padx=(0, 10))
|
|
tk.Button(button_frame, text="📁 Folder", command=select_folder,
|
|
width=12, font=("Arial", 10)).pack(side="left", padx=(0, 10))
|
|
tk.Button(button_frame, text="Cancel", command=cancel_selection,
|
|
width=12).pack(side="right")
|
|
|
|
def show_transfer_stats(self, stats: dict, operation: str):
|
|
"""Show transfer statistics in a dialog.
|
|
|
|
Args:
|
|
stats: Dictionary containing transfer statistics
|
|
operation: Description of the operation performed
|
|
"""
|
|
if not stats:
|
|
return
|
|
|
|
stats_window = tk.Toplevel(self.parent)
|
|
stats_window.title("Transfer Statistics")
|
|
stats_window.geometry("400x300")
|
|
stats_window.resizable(True, True)
|
|
stats_window.transient(self.parent)
|
|
stats_window.grab_set()
|
|
|
|
# Center the window
|
|
stats_window.geometry("+{}+{}".format(
|
|
self.parent.winfo_x() + 60,
|
|
self.parent.winfo_y() + 60
|
|
))
|
|
|
|
main_frame = tk.Frame(stats_window)
|
|
main_frame.pack(fill="both", expand=True, padx=15, pady=15)
|
|
|
|
# Title
|
|
title_label = tk.Label(main_frame, text=f"📊 Transfer Complete - {operation.title()}",
|
|
font=("Arial", 14, "bold"))
|
|
title_label.pack(pady=(0, 15))
|
|
|
|
# Stats display
|
|
stats_text = tk.Text(main_frame, wrap=tk.WORD, font=("Courier", 10),
|
|
height=12, width=50)
|
|
stats_text.pack(fill="both", expand=True, pady=(0, 15))
|
|
|
|
# Format and insert stats
|
|
stats_content = []
|
|
for key, value in stats.items():
|
|
formatted_key = key.replace('_', ' ').title()
|
|
stats_content.append(f"{formatted_key}: {value}")
|
|
|
|
stats_text.insert(tk.END, "\n".join(stats_content))
|
|
stats_text.config(state=tk.DISABLED)
|
|
|
|
# Close button
|
|
tk.Button(main_frame, text="Close",
|
|
command=stats_window.destroy).pack()
|
|
|
|
def show_disable_debugging_reminder(self):
|
|
"""Show reminder about disabling USB debugging after transfer."""
|
|
messagebox.showinfo(
|
|
"Security Reminder",
|
|
"Transfer complete!\n\nFor security, consider disabling USB debugging when not needed:\n"
|
|
"Settings → Developer Options → USB debugging (toggle off)\n\n"
|
|
"Keep it enabled if you plan to transfer files again soon."
|
|
) |