Files
android-assistant/src/gui/components/ui_components.py
T

310 lines
9.2 KiB
Python

"""
UI Components Module
Contains reusable UI components for the Android file transfer application.
"""
import tkinter as tk
from tkinter import messagebox
from typing import Callable, Optional
try:
from ..dialogs.license_agreement import (
LicenseAgreementFrame,
check_license_agreement,
)
except ImportError:
from gui.dialogs.license_agreement import (
LicenseAgreementFrame,
check_license_agreement,
)
class PathSelectorFrame:
"""Frame component for path selection with browse button."""
def __init__(self, parent: tk.Widget, label_text: str, browse_command: Callable):
"""Initialize the path selector frame.
Args:
parent: Parent widget
label_text: Text for the label
browse_command: Command to execute when browse button is clicked
"""
self.frame = tk.Frame(parent)
self.label = tk.Label(self.frame, text=label_text)
self.label.pack(anchor="w")
# Path frame with label and button
path_frame = tk.Frame(self.frame)
path_frame.pack(fill="x", pady=(0, 10))
self.path_var = tk.StringVar(value="Please select file or folder ->")
self.path_display = tk.Label(path_frame, textvariable=self.path_var, anchor="w")
self.path_display.pack(side="left", fill="x", expand=True)
self.browse_btn = tk.Button(
path_frame, text="Browse...", command=browse_command
)
self.browse_btn.pack(side="right", padx=(5, 0))
def pack(self, **kwargs):
"""Pack the frame."""
self.frame.pack(**kwargs)
def pack_forget(self):
"""Remove the frame from packing."""
self.frame.pack_forget()
def set_path(self, path: str) -> None:
"""Set the displayed path.
Args:
path: Path to display
"""
self.path_var.set(path)
def get_path(self) -> str:
"""Get the current path.
Returns:
Current path string
"""
return self.path_var.get().strip()
def is_path_selected(self) -> bool:
"""Check if a valid path is selected.
Returns:
True if path is selected and not the default placeholder
"""
path = self.get_path()
return path and path != "Please select file or folder ->"
def clear_path(self) -> None:
"""Clear the path selection."""
self.path_var.set("Please select file or folder ->")
def enable_browse(self) -> None:
"""Enable the browse button."""
self.browse_btn.config(state="normal")
def disable_browse(self) -> None:
"""Disable the browse button."""
self.browse_btn.config(state="disabled")
class DirectionSelector:
"""Component for selecting transfer direction (pull/push)."""
def __init__(self, parent: tk.Widget, on_change_command: Optional[Callable] = None):
"""Initialize the direction selector.
Args:
parent: Parent widget
on_change_command: Command to execute when direction changes
"""
self.direction_var = tk.StringVar(value="pull")
self.frame = tk.Frame(parent)
self.frame.pack(anchor="w", padx=10, pady=(10, 0))
tk.Radiobutton(
self.frame,
text="Pull (Android → Computer)",
variable=self.direction_var,
value="pull",
command=on_change_command,
).pack(side="left")
tk.Radiobutton(
self.frame,
text="Push (Computer → Android)",
variable=self.direction_var,
value="push",
command=on_change_command,
).pack(side="left", padx=(20, 0))
def get_direction(self) -> str:
"""Get the current direction.
Returns:
Current direction ('pull' or 'push')
"""
return self.direction_var.get()
class StatusLabel:
"""Responsive status label with word wrapping."""
def __init__(self, parent: tk.Widget, initial_text: str = "Status: Idle"):
"""Initialize the status label.
Args:
parent: Parent widget
initial_text: Initial status text
"""
self.label = tk.Label(
parent,
text=initial_text,
wraplength=0, # Will be set dynamically
justify="center",
anchor="center",
)
self.label.pack(padx=10, fill="x", pady=(20, 5))
# Bind parent window resize to update wrapping
parent.bind("<Configure>", self._on_window_configure)
def set_text(self, text: str) -> None:
"""Set the status text.
Args:
text: Text to display
"""
self.label.config(text=text)
def get_text(self) -> str:
"""Get the current status text.
Returns:
Current status text
"""
return self.label.cget("text")
def _on_window_configure(self, event) -> None:
"""Handle window resize events to update label wrapping.
Args:
event: Configure event
"""
# Only handle configure events for the main window, not child widgets
if (
hasattr(event.widget, "winfo_toplevel")
and event.widget == event.widget.winfo_toplevel()
):
# Calculate available width for the status label
# Account for padding (10px on each side) and some margin
available_width = event.widget.winfo_width() - 40
if available_width > 100: # Minimum reasonable width
self.label.config(wraplength=available_width)
class TransferButton:
"""Multi-mode transfer button that changes text and behavior based on state."""
def __init__(self, parent: tk.Widget):
"""Initialize the transfer button.
Args:
parent: Parent widget
"""
self.button = tk.Button(parent, text="Start Transfer", state="disabled")
self.button.pack(pady=10)
self.current_mode = "transfer" # transfer, recheck, cancel
def set_transfer_mode(self, command: Callable, enabled: bool = True) -> None:
"""Set button to transfer mode.
Args:
command: Command to execute on button click
enabled: Whether button should be enabled
"""
self.current_mode = "transfer"
self.button.config(
text="Start Transfer",
command=command,
state="normal" if enabled else "disabled",
)
def set_recheck_mode(self, command: Callable) -> None:
"""Set button to recheck device mode.
Args:
command: Command to execute on button click
"""
self.current_mode = "recheck"
self.button.config(
text="Recheck for connected Android device", command=command, state="normal"
)
def set_cancel_mode(self, command: Callable) -> None:
"""Set button to cancel transfer mode.
Args:
command: Command to execute on button click
"""
self.current_mode = "cancel"
self.button.config(text="Cancel Transfer", command=command, state="normal")
def set_checking_mode(self) -> None:
"""Set button to temporary checking state."""
self.button.config(text="Checking...", state="disabled")
def enable(self) -> None:
"""Enable the button."""
self.button.config(state="normal")
def disable(self) -> None:
"""Disable the button."""
self.button.config(state="disabled")
def get_mode(self) -> str:
"""Get the current button mode.
Returns:
Current mode string ('transfer', 'recheck', 'cancel')
"""
return self.current_mode
class LicenseManager:
"""Manages license agreement display and main interface switching."""
def __init__(self, parent_window: tk.Tk):
"""Initialize the license manager.
Args:
parent_window: The main window instance
"""
self.parent = parent_window
self.license_agreed = check_license_agreement()
self.license_frame = None
self.on_agreed_callback: Optional[Callable] = None
def needs_license_agreement(self) -> bool:
"""Check if license agreement is needed.
Returns:
True if license agreement needs to be shown, False otherwise
"""
return not self.license_agreed
def show_license_agreement(self, on_agreed_callback: Callable) -> None:
"""Show the license agreement interface.
Args:
on_agreed_callback: Callback to execute when license is agreed
"""
self.on_agreed_callback = on_agreed_callback
# Adjust window size for license agreement
self.parent.geometry("700x600")
self.parent.minsize(700, 600)
# Create license agreement frame
self.license_frame = LicenseAgreementFrame(self.parent, self._on_license_agreed)
def _on_license_agreed(self) -> None:
"""Handle when user agrees to license."""
self.license_agreed = True
# Remove license frame
if self.license_frame:
self.license_frame.destroy()
self.license_frame = None
# Execute callback
if self.on_agreed_callback:
self.on_agreed_callback()