Files
android-assistant/src/gui/dialogs/dialog_manager.py
T

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."
)