802 lines
34 KiB
Python
802 lines
34 KiB
Python
import os
|
|
import shutil
|
|
import tkinter as tk
|
|
import subprocess
|
|
import json
|
|
import threading
|
|
import webbrowser
|
|
from typing import Optional, List, Tuple, Dict, Union
|
|
|
|
import requests
|
|
|
|
from shared_libs.common_tools import IconManager, Tooltip, LxTools, message_box_animation
|
|
from shared_libs.gitea import GiteaUpdater, GiteaApiUrlError, GiteaVersionParseError, GiteaApiResponseError
|
|
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
|
from .cfd_ui_setup import StyleManager, WidgetManager
|
|
from shared_libs.animated_icon import AnimatedIcon
|
|
from .cfd_settings_dialog import SettingsDialog
|
|
from .cfd_file_operations import FileOperationsManager
|
|
from .cfd_search_manager import SearchManager
|
|
from .cfd_navigation_manager import NavigationManager
|
|
from .cfd_view_manager import ViewManager
|
|
from .cfd_sftp_manager import SFTPManager, PARAMIKO_AVAILABLE
|
|
from shared_libs.message import CredentialsDialog, MessageDialog, InputDialog
|
|
|
|
|
|
class CustomFileDialog(tk.Toplevel):
|
|
"""
|
|
A custom file dialog window that provides functionalities for file selection,
|
|
directory navigation, search, and file operations.
|
|
"""
|
|
|
|
def __init__(self, parent: tk.Widget, initial_dir: Optional[str] = None,
|
|
filetypes: Optional[List[Tuple[str, str]]] = None,
|
|
mode: str = "open", title: str = LocaleStrings.CFD["title"]):
|
|
"""
|
|
Initializes the CustomFileDialog.
|
|
"""
|
|
super().__init__(parent)
|
|
self.current_fs_type = "local" # "local" or "sftp"
|
|
|
|
self.sftp_manager = SFTPManager()
|
|
self.config_manager = CfdConfigManager()
|
|
|
|
self.my_tool_tip: Optional[Tooltip] = None
|
|
self.dialog_mode: str = mode
|
|
self.gitea_api_url = CfdConfigManager.UPDATE_URL
|
|
self.lib_version = CfdConfigManager.VERSION
|
|
self.update_status: str = ""
|
|
|
|
self.load_settings()
|
|
|
|
self.geometry(self.settings["window_size_preset"])
|
|
min_width, min_height = self.get_min_size_from_preset(
|
|
self.settings["window_size_preset"])
|
|
self.minsize(min_width, min_height)
|
|
|
|
self.title(title)
|
|
self.image: IconManager = IconManager()
|
|
width, height = map(
|
|
int, self.settings["window_size_preset"].split('x'))
|
|
LxTools.center_window_cross_platform(self, width, height)
|
|
self.parent: tk.Widget = parent
|
|
self.transient(parent)
|
|
self.grab_set()
|
|
|
|
self.result: Optional[Union[str, List[str]]] = None
|
|
self.current_dir: str = os.path.abspath(
|
|
initial_dir) if initial_dir else os.path.expanduser("~")
|
|
self.filetypes: List[Tuple[str, str]] = filetypes if filetypes else [
|
|
(LocaleStrings.CFD["all_files"], "*.* ")]
|
|
self.current_filter_pattern: str = self.filetypes[0][1]
|
|
self.history: List[str] = []
|
|
self.history_pos: int = -1
|
|
self.view_mode: tk.StringVar = tk.StringVar(
|
|
value=self.settings.get("default_view_mode", "icons"))
|
|
self.show_hidden_files: tk.BooleanVar = tk.BooleanVar(value=False)
|
|
self.resize_job: Optional[str] = None
|
|
self.last_width: int = 0
|
|
self.selected_item_frames: List[ttk.Frame] = []
|
|
self.search_results: List[str] = []
|
|
self.search_mode: bool = False
|
|
self.original_path_text: str = ""
|
|
self.items_to_load_per_batch: int = 250
|
|
self.item_path_map: Dict[int, str] = {}
|
|
self.responsive_buttons_hidden: Optional[bool] = None
|
|
self.search_job: Optional[str] = None
|
|
self.search_thread: Optional[threading.Thread] = None
|
|
self.search_process: Optional[subprocess.Popen] = None
|
|
|
|
self.icon_manager: IconManager = IconManager()
|
|
self._initialize_managers()
|
|
|
|
self.widget_manager.filename_entry.bind(
|
|
"<Return>", self.search_manager.execute_search)
|
|
|
|
self.update_animation_settings()
|
|
|
|
self.view_manager._update_view_mode_buttons()
|
|
|
|
def initial_load() -> None:
|
|
"""Performs the initial loading and UI setup."""
|
|
self.update_idletasks()
|
|
self.last_width = self.widget_manager.file_list_frame.winfo_width()
|
|
self._handle_responsive_buttons(self.winfo_width())
|
|
self.navigation_manager.navigate_to(self.current_dir)
|
|
|
|
self.after(10, initial_load)
|
|
|
|
self.widget_manager.path_entry.bind(
|
|
"<Return>", self.navigation_manager.handle_path_entry_return)
|
|
|
|
self.widget_manager.home_button.config(command=self.go_to_local_home)
|
|
|
|
self.bind("<Key>", self.search_manager.show_search_bar)
|
|
|
|
if self.dialog_mode == "save":
|
|
self.bind("<Delete>", self.file_op_manager.delete_selected_item)
|
|
|
|
if self.gitea_api_url and self.lib_version:
|
|
self.update_thread = threading.Thread(
|
|
target=self.check_for_updates, daemon=True)
|
|
self.update_thread.start()
|
|
|
|
def _initialize_managers(self) -> None:
|
|
"""Initializes or re-initializes all the manager classes."""
|
|
self.style_manager: StyleManager = StyleManager(self)
|
|
self.file_op_manager: FileOperationsManager = FileOperationsManager(
|
|
self)
|
|
self.search_manager: SearchManager = SearchManager(self)
|
|
self.navigation_manager: NavigationManager = NavigationManager(self)
|
|
self.view_manager: ViewManager = ViewManager(self)
|
|
self.widget_manager: WidgetManager = WidgetManager(self, self.settings)
|
|
|
|
def load_settings(self) -> None:
|
|
"""Loads settings from the configuration file."""
|
|
self.settings = CfdConfigManager.load()
|
|
size_preset = self.settings.get("window_size_preset", "1050x850")
|
|
self.settings["window_size_preset"] = size_preset
|
|
if hasattr(self, 'view_mode'):
|
|
self.view_mode.set(self.settings.get("default_view_mode", "icons"))
|
|
|
|
def get_min_size_from_preset(self, preset: str) -> Tuple[int, int]:
|
|
"""
|
|
Calculates the minimum window size based on a preset string.
|
|
"""
|
|
w, h = map(int, preset.split('x'))
|
|
return max(650, w - 400), max(450, h - 400)
|
|
|
|
def reload_config_and_rebuild_ui(self) -> None:
|
|
"""Reloads the configuration and rebuilds the entire UI."""
|
|
is_sftp_connected = (self.current_fs_type == "sftp")
|
|
|
|
self.load_settings()
|
|
|
|
self.geometry(self.settings["window_size_preset"])
|
|
min_width, min_height = self.get_min_size_from_preset(
|
|
self.settings["window_size_preset"])
|
|
self.minsize(min_width, min_height)
|
|
width, height = map(
|
|
int, self.settings["window_size_preset"].split('x'))
|
|
LxTools.center_window_cross_platform(self, width, height)
|
|
|
|
for widget in self.winfo_children():
|
|
widget.destroy()
|
|
|
|
self._initialize_managers()
|
|
|
|
if is_sftp_connected:
|
|
self.widget_manager.sftp_button.config(
|
|
command=self.disconnect_sftp, style="Header.TButton.Active.Round")
|
|
|
|
self.widget_manager.filename_entry.bind(
|
|
"<Return>", self.search_manager.execute_search)
|
|
self.view_manager._update_view_mode_buttons()
|
|
|
|
self.responsive_buttons_hidden = None
|
|
self.update_idletasks()
|
|
self._handle_responsive_buttons(self.winfo_width())
|
|
|
|
self.update_animation_settings()
|
|
|
|
if self.search_mode:
|
|
self.search_manager.show_search_results_treeview()
|
|
else:
|
|
self.navigation_manager.navigate_to(self.current_dir)
|
|
|
|
def open_settings_dialog(self) -> None:
|
|
"""Opens the settings dialog."""
|
|
SettingsDialog(self, dialog_mode=self.dialog_mode)
|
|
|
|
def open_sftp_dialog(self):
|
|
if not PARAMIKO_AVAILABLE:
|
|
MessageDialog(message_type="error",
|
|
text="Paramiko library is not installed.").show()
|
|
return
|
|
|
|
dialog = CredentialsDialog(self)
|
|
credentials = dialog.show()
|
|
|
|
if credentials:
|
|
self.connect_sftp(credentials, is_new_connection=True)
|
|
|
|
def connect_sftp(self, credentials, is_new_connection: bool = False):
|
|
self.config(cursor="watch")
|
|
self.update_idletasks()
|
|
|
|
if is_new_connection and credentials.get("save_bookmark"):
|
|
bookmark_name = credentials["bookmark_name"]
|
|
bookmark_data = {
|
|
"host": credentials["host"],
|
|
"port": credentials["port"],
|
|
"username": credentials["username"],
|
|
"initial_path": credentials["initial_path"],
|
|
"key_file": credentials["key_file"],
|
|
}
|
|
try:
|
|
import keyring
|
|
service_name = f"customfiledialog-sftp"
|
|
if credentials["password"]:
|
|
keyring.set_password(service_name, f"{bookmark_name}_password", credentials["password"])
|
|
bookmark_data["password_in_keyring"] = True
|
|
if credentials["passphrase"]:
|
|
keyring.set_password(service_name, f"{bookmark_name}_passphrase", credentials["passphrase"])
|
|
bookmark_data["passphrase_in_keyring"] = True
|
|
|
|
self.config_manager.add_bookmark(bookmark_name, bookmark_data)
|
|
self.after(100, self.reload_config_and_rebuild_ui)
|
|
|
|
except Exception as e:
|
|
MessageDialog(message_type="error", text=f"Could not save bookmark: {e}").show()
|
|
|
|
success, message = self.sftp_manager.connect(
|
|
host=credentials.get('host'),
|
|
port=credentials.get('port'),
|
|
username=credentials.get('username'),
|
|
password=credentials.get('password'),
|
|
key_file=credentials.get('key_file'),
|
|
passphrase=credentials.get('passphrase')
|
|
)
|
|
|
|
self.config(cursor="")
|
|
|
|
if success:
|
|
self.current_fs_type = "sftp"
|
|
self.widget_manager.sftp_button.config(
|
|
command=self.disconnect_sftp, style="Header.TButton.Active.Round")
|
|
|
|
initial_path = credentials.get("initial_path", "/")
|
|
self.navigation_manager.navigate_to(initial_path)
|
|
else:
|
|
MessageDialog(message_type="error",
|
|
text=f"Connection failed: {message}").show()
|
|
|
|
def connect_sftp_bookmark(self, data):
|
|
credentials = data.copy()
|
|
try:
|
|
import keyring
|
|
service_name = f"customfiledialog-sftp"
|
|
bookmark_name = next(name for name, b_data in self.config_manager.load_bookmarks().items() if b_data == data)
|
|
|
|
if credentials.get("password_in_keyring"):
|
|
credentials["password"] = keyring.get_password(service_name, f"{bookmark_name}_password")
|
|
if credentials.get("passphrase_in_keyring"):
|
|
credentials["passphrase"] = keyring.get_password(service_name, f"{bookmark_name}_passphrase")
|
|
|
|
except (ImportError, StopIteration, Exception) as e:
|
|
MessageDialog(message_type="error", text=f"Could not retrieve credentials: {e}").show()
|
|
return
|
|
|
|
self.connect_sftp(credentials, is_new_connection=False)
|
|
|
|
def edit_sftp_bookmark(self, name: str, data: dict):
|
|
"""Opens the credentials dialog to edit an existing SFTP bookmark."""
|
|
data['bookmark_name'] = name
|
|
dialog = CredentialsDialog(self, title=f"Edit Bookmark: {name}", initial_data=data, is_edit_mode=True)
|
|
new_data = dialog.show()
|
|
|
|
if new_data:
|
|
self.remove_sftp_bookmark(name, confirm=False)
|
|
self.connect_sftp(new_data, is_new_connection=True)
|
|
|
|
def remove_sftp_bookmark(self, name: str, confirm: bool = True):
|
|
"""Removes an SFTP bookmark and its credentials from the keyring."""
|
|
do_remove = False
|
|
if confirm:
|
|
confirm_dialog = MessageDialog(
|
|
message_type="ask",
|
|
text=f"Remove bookmark '{name}'?",
|
|
buttons=["Yes", "No"])
|
|
if confirm_dialog.show():
|
|
do_remove = True
|
|
else:
|
|
do_remove = True
|
|
|
|
if do_remove:
|
|
try:
|
|
import keyring
|
|
service_name = f"customfiledialog-sftp"
|
|
try:
|
|
keyring.delete_password(service_name, f"{name}_password")
|
|
except keyring.errors.PasswordDeleteError:
|
|
pass
|
|
try:
|
|
keyring.delete_password(service_name, f"{name}_passphrase")
|
|
except keyring.errors.PasswordDeleteError:
|
|
pass
|
|
except ImportError:
|
|
pass
|
|
except Exception as e:
|
|
print(f"Could not remove credentials from keyring for {name}: {e}")
|
|
|
|
self.config_manager.remove_bookmark(name)
|
|
self.reload_config_and_rebuild_ui()
|
|
|
|
def disconnect_sftp(self, path_to_navigate_to: Optional[str] = None):
|
|
self.sftp_manager.disconnect()
|
|
self.current_fs_type = "local"
|
|
self.widget_manager.sftp_button.config(
|
|
command=self.open_sftp_dialog, style="Header.TButton.Borderless.Round")
|
|
target_path = path_to_navigate_to if path_to_navigate_to else os.path.expanduser("~")
|
|
self.navigation_manager.navigate_to(target_path)
|
|
|
|
def go_to_local_home(self):
|
|
if self.current_fs_type == "sftp":
|
|
self.disconnect_sftp()
|
|
else:
|
|
self.navigation_manager.navigate_to(os.path.expanduser("~"))
|
|
|
|
def handle_sidebar_bookmark_click(self, local_path: str):
|
|
if self.current_fs_type == "sftp":
|
|
self.disconnect_sftp(path_to_navigate_to=local_path)
|
|
else:
|
|
self.navigation_manager.navigate_to(local_path)
|
|
|
|
def update_animation_settings(self) -> None:
|
|
"""Updates the search animation icon based on current settings."""
|
|
use_pillow = self.settings.get('use_pillow_animation', False)
|
|
anim_type = self.settings.get('animation_type', 'double')
|
|
is_running = self.widget_manager.search_animation.running
|
|
if is_running:
|
|
self.widget_manager.search_animation.stop()
|
|
|
|
self.widget_manager.search_animation.destroy()
|
|
self.widget_manager.search_animation = AnimatedIcon(
|
|
self.widget_manager.status_container,
|
|
width=23,
|
|
height=23,
|
|
use_pillow=use_pillow,
|
|
animation_type=anim_type,
|
|
color="#2a6fde",
|
|
highlight_color="#5195ff",
|
|
bg=self.style_manager.bottom_color
|
|
)
|
|
self.widget_manager.search_animation.grid(
|
|
row=0, column=0, sticky='w', padx=(0, 5), pady=(4, 0))
|
|
self.widget_manager.search_animation.bind(
|
|
"<Button-1>", lambda e: self.search_manager.activate_search())
|
|
|
|
self.my_tool_tip = Tooltip(
|
|
self.widget_manager.search_animation,
|
|
text=lambda: LocaleStrings.UI["cancel_search"] if self.widget_manager.search_animation.running else LocaleStrings.UI["start_search"]
|
|
)
|
|
|
|
if is_running:
|
|
self.widget_manager.search_animation.start()
|
|
|
|
def check_for_updates(self) -> None:
|
|
"""Checks for library updates via the Gitea API in a background thread."""
|
|
try:
|
|
new_version = GiteaUpdater.check_for_update(
|
|
self.gitea_api_url,
|
|
self.lib_version,
|
|
)
|
|
self.after(0, self.update_ui_for_update, new_version)
|
|
except (requests.exceptions.RequestException, GiteaApiUrlError, GiteaVersionParseError, GiteaApiResponseError):
|
|
self.after(0, self.update_ui_for_update, "ERROR")
|
|
except Exception:
|
|
self.after(0, self.update_ui_for_update, "ERROR")
|
|
|
|
def _run_installer(self, event: Optional[tk.Event] = None) -> None:
|
|
"""Runs the LxTools installer if it exists."""
|
|
installer_path = '/usr/local/bin/lxtools_installer'
|
|
if os.path.exists(installer_path):
|
|
try:
|
|
subprocess.Popen([installer_path])
|
|
self.widget_manager.search_status_label.config(
|
|
text="Installer started...")
|
|
except OSError as e:
|
|
self.widget_manager.search_status_label.config(
|
|
text=f"Error starting installer: {e}")
|
|
else:
|
|
self.widget_manager.search_status_label.config(
|
|
text=f"Installer not found at {installer_path}")
|
|
|
|
def update_ui_for_update(self, new_version: Optional[str]) -> None:
|
|
"""
|
|
Updates the UI based on the result of the library update check.
|
|
"""
|
|
self.update_status = new_version
|
|
icon = self.widget_manager.update_animation_icon
|
|
icon.grid_remove()
|
|
icon.hide()
|
|
|
|
if new_version is None or new_version == "ERROR":
|
|
return
|
|
|
|
icon.grid(row=0, column=2, sticky='e', padx=(10, 5))
|
|
tooltip_msg = LocaleStrings.UI["install_new_version"].format(
|
|
version=new_version)
|
|
icon.start()
|
|
|
|
icon.bind("<Button-1>", self._run_installer)
|
|
Tooltip(icon, tooltip_msg)
|
|
|
|
def get_file_icon(self, filename: str, size: str = 'large') -> tk.PhotoImage:
|
|
"""
|
|
Gets the appropriate icon for a given filename.
|
|
"""
|
|
ext = os.path.splitext(filename)[1].lower()
|
|
|
|
if ext == '.py':
|
|
return self.icon_manager.get_icon(f'python_{size}')
|
|
if ext == '.pdf':
|
|
return self.icon_manager.get_icon(f'pdf_{size}')
|
|
if ext in ['.tar', '.zip', '.rar', '.7z', '.gz']:
|
|
return self.icon_manager.get_icon(f'archive_{size}')
|
|
if ext in ['.mp3', '.wav', '.ogg', '.flac']:
|
|
return self.icon_manager.get_icon(f'audio_{size}')
|
|
if ext in ['.mp4', '.mkv', '.avi', '.mov']:
|
|
return self.icon_manager.get_icon(f'video_{size}') if size == 'large' else self.icon_manager.get_icon(
|
|
'video_small_file')
|
|
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg']:
|
|
return self.icon_manager.get_icon(f'picture_{size}')
|
|
if ext == '.iso':
|
|
return self.icon_manager.get_icon(f'iso_{size}')
|
|
return self.icon_manager.get_icon(f'file_{size}')
|
|
|
|
def on_window_resize(self, event: tk.Event) -> None:
|
|
"""
|
|
Handles the window resize event.
|
|
"""
|
|
if event.widget is self:
|
|
if self.view_mode.get() == "icons" and not self.search_mode:
|
|
new_width = self.widget_manager.file_list_frame.winfo_width()
|
|
if abs(new_width - self.last_width) > 50:
|
|
if self.resize_job:
|
|
self.after_cancel(self.resize_job)
|
|
|
|
def repopulate_icons() -> None:
|
|
"""Repopulates the file list icons."""
|
|
self.update_idletasks()
|
|
self.view_manager.populate_files()
|
|
|
|
self.resize_job = self.after(150, repopulate_icons)
|
|
self.last_width = new_width
|
|
|
|
self._handle_responsive_buttons(event.width)
|
|
|
|
def _handle_responsive_buttons(self, window_width: int) -> None:
|
|
"""
|
|
Shows or hides buttons based on the window width.
|
|
"""
|
|
threshold = 850
|
|
container = self.widget_manager.responsive_buttons_container
|
|
more_button = self.widget_manager.more_button
|
|
|
|
should_be_hidden = window_width < threshold
|
|
|
|
if should_be_hidden != self.responsive_buttons_hidden:
|
|
if should_be_hidden:
|
|
container.pack_forget()
|
|
more_button.pack(side="left", padx=5)
|
|
else:
|
|
more_button.pack_forget()
|
|
container.pack(side="left")
|
|
self.responsive_buttons_hidden = should_be_hidden
|
|
|
|
def show_more_menu(self) -> None:
|
|
"""Displays a 'more options' menu."""
|
|
more_menu = tk.Menu(self, tearoff=0, background=self.style_manager.header, foreground=self.style_manager.color_foreground,
|
|
activebackground=self.style_manager.selection_color, activeforeground=self.style_manager.color_foreground, relief='flat', borderwidth=0)
|
|
|
|
is_writable = os.access(self.current_dir, os.W_OK)
|
|
creation_state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED
|
|
|
|
more_menu.add_command(label=LocaleStrings.UI["new_folder"], command=self.file_op_manager.create_new_folder,
|
|
image=self.icon_manager.get_icon('new_folder_small'), compound='left', state=creation_state)
|
|
more_menu.add_command(label=LocaleStrings.UI["new_document"], command=self.file_op_manager.create_new_file,
|
|
image=self.icon_manager.get_icon('new_document_small'), compound='left', state=creation_state)
|
|
more_menu.add_separator()
|
|
more_menu.add_command(label=LocaleStrings.VIEW["icon_view"], command=self.view_manager.set_icon_view,
|
|
image=self.icon_manager.get_icon('icon_view'), compound='left')
|
|
more_menu.add_command(label=LocaleStrings.VIEW["list_view"], command=self.view_manager.set_list_view,
|
|
image=self.icon_manager.get_icon('list_view'), compound='left')
|
|
more_menu.add_separator()
|
|
|
|
hidden_files_label = LocaleStrings.UI["hide_hidden_files"] if self.show_hidden_files.get(
|
|
) else LocaleStrings.UI["show_hidden_files"]
|
|
hidden_files_icon = self.icon_manager.get_icon(
|
|
'unhide') if self.show_hidden_files.get() else self.icon_manager.get_icon('hide')
|
|
more_menu.add_command(label=hidden_files_label, command=self.view_manager.toggle_hidden_files,
|
|
image=hidden_files_icon, compound='left')
|
|
|
|
more_button = self.widget_manager.more_button
|
|
x = more_button.winfo_rootx()
|
|
y = more_button.winfo_rooty() + more_button.winfo_height()
|
|
more_menu.tk_popup(x, y)
|
|
|
|
def on_sidebar_resize(self, event: tk.Event) -> None:
|
|
"""
|
|
Handles the sidebar resize event, adjusting button text visibility.
|
|
"""
|
|
current_width = event.width
|
|
threshold_width = 100
|
|
|
|
if current_width < threshold_width:
|
|
for btn, original_text in self.widget_manager.sidebar_buttons:
|
|
btn.config(text="", compound="top")
|
|
for btn, original_text in self.widget_manager.device_buttons:
|
|
btn.config(text="", compound="top")
|
|
else:
|
|
for btn, original_text in self.widget_manager.sidebar_buttons:
|
|
btn.config(text=original_text, compound="left")
|
|
for btn, original_text in self.widget_manager.device_buttons:
|
|
btn.config(text=original_text, compound="left")
|
|
|
|
def _on_devices_enter(self, event: tk.Event) -> None:
|
|
"""
|
|
Shows the scrollbar when the mouse enters the devices area.
|
|
"""
|
|
self.widget_manager.devices_scrollbar.grid(
|
|
row=1, column=1, sticky="ns")
|
|
|
|
def _on_devices_leave(self, event: tk.Event) -> None:
|
|
"""
|
|
Hides the scrollbar when the mouse leaves the devices area.
|
|
"""
|
|
x, y = event.x_root, event.y_root
|
|
widget_x = self.widget_manager.devices_canvas.winfo_rootx()
|
|
widget_y = self.widget_manager.devices_canvas.winfo_rooty()
|
|
widget_width = self.widget_manager.devices_canvas.winfo_width()
|
|
widget_height = self.widget_manager.devices_canvas.winfo_height()
|
|
|
|
buffer = 5
|
|
if not (widget_x - buffer <= x <= widget_x + widget_width + buffer and
|
|
widget_y - buffer <= y <= widget_y + widget_height + buffer):
|
|
self.widget_manager.devices_scrollbar.grid_remove()
|
|
|
|
def toggle_recursive_search(self) -> None:
|
|
"""
|
|
Toggles the recursive search option on or off."""
|
|
self.widget_manager.recursive_search.set(
|
|
not self.widget_manager.recursive_search.get())
|
|
if self.widget_manager.recursive_search.get():
|
|
self.widget_manager.recursive_button.configure(
|
|
style="Header.TButton.Active.Round")
|
|
else:
|
|
self.widget_manager.recursive_button.configure(
|
|
style="Header.TButton.Borderless.Round")
|
|
|
|
def update_selection_info(self, status_info: Optional[str] = None) -> None:
|
|
"""
|
|
Updates status bar, filename entry, and result based on current selection.
|
|
"""
|
|
self._update_disk_usage()
|
|
status_text = ""
|
|
is_sftp = self.current_fs_type == 'sftp'
|
|
|
|
# Helper to get basename safely
|
|
def get_basename(path):
|
|
if not path:
|
|
return ""
|
|
if is_sftp:
|
|
return path.split('/')[-1]
|
|
return os.path.basename(path)
|
|
|
|
if self.dialog_mode == 'multi':
|
|
selected_paths = self.result if isinstance(
|
|
self.result, list) else []
|
|
self.widget_manager.filename_entry.delete(0, tk.END)
|
|
if selected_paths:
|
|
filenames = [
|
|
f'"{get_basename(p)}"' for p in selected_paths]
|
|
self.widget_manager.filename_entry.insert(
|
|
0, " ".join(filenames))
|
|
count = len(selected_paths)
|
|
status_text = f"{count} {LocaleStrings.CFD['items_selected']}"
|
|
else:
|
|
status_text = ""
|
|
else:
|
|
path_exists = False
|
|
if status_info:
|
|
if is_sftp:
|
|
path_exists = self.sftp_manager.exists(status_info)
|
|
else:
|
|
path_exists = os.path.exists(status_info)
|
|
|
|
if status_info and path_exists:
|
|
self.result = status_info
|
|
basename = get_basename(status_info)
|
|
self.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.widget_manager.filename_entry.insert(0, basename)
|
|
|
|
if self.view_manager._is_dir(status_info):
|
|
content_count = self.view_manager._get_folder_content_count(status_info)
|
|
if content_count is not None:
|
|
status_text = f"'{basename}' ({content_count} {LocaleStrings.CFD['entries']})"
|
|
else:
|
|
status_text = f"'{basename}'"
|
|
else:
|
|
status_text = f"'{basename}'"
|
|
elif status_info:
|
|
status_text = status_info
|
|
|
|
self.widget_manager.search_status_label.config(text=status_text)
|
|
self.update_action_buttons_state()
|
|
|
|
def _update_disk_usage(self) -> None:
|
|
"""Updates only the disk usage part of the status bar."""
|
|
if self.current_fs_type == "sftp":
|
|
self.widget_manager.storage_label.config(text="SFTP Storage: N/A")
|
|
self.widget_manager.storage_bar['value'] = 0
|
|
return
|
|
try:
|
|
# This can fail on certain file types like symlinks to other filesystems.
|
|
total, used, free = shutil.disk_usage(self.current_dir)
|
|
free_str = self._format_size(free)
|
|
self.widget_manager.storage_label.config(
|
|
text=f"{LocaleStrings.CFD['free_space']}: {free_str}")
|
|
self.widget_manager.storage_bar['value'] = (used / total) * 100
|
|
except (FileNotFoundError, PermissionError):
|
|
# If disk usage cannot be determined, just show N/A instead of an error.
|
|
self.widget_manager.storage_label.config(
|
|
text=f"{LocaleStrings.CFD['free_space']}: N/A")
|
|
self.widget_manager.storage_bar['value'] = 0
|
|
|
|
def on_open(self) -> None:
|
|
"""Handles the 'Open' or 'OK' action based on the dialog mode."""
|
|
if self.dialog_mode == 'multi':
|
|
if self.result and isinstance(self.result, list) and self.result:
|
|
self.destroy()
|
|
return
|
|
|
|
selected_path = self.result
|
|
if not selected_path or not isinstance(selected_path, str):
|
|
return
|
|
|
|
if self.dialog_mode == 'dir':
|
|
if self.view_manager._is_dir(selected_path):
|
|
self.destroy()
|
|
elif self.dialog_mode == 'open':
|
|
if not self.view_manager._is_dir(selected_path):
|
|
self.destroy()
|
|
|
|
def on_save(self) -> None:
|
|
"""Handles the 'Save' action, setting the selected file and closing the dialog."""
|
|
file_name = self.widget_manager.filename_entry.get()
|
|
if file_name:
|
|
self.result = os.path.join(self.current_dir, file_name)
|
|
self.destroy()
|
|
|
|
def on_cancel(self) -> None:
|
|
"""Handles the 'Cancel' action, clearing the selection and closing the dialog."""
|
|
self.result = None
|
|
self.destroy()
|
|
|
|
def get_result(self) -> Optional[Union[str, List[str]]]:
|
|
"""Returns the result of the dialog."""
|
|
return self.result
|
|
|
|
def update_action_buttons_state(self) -> None:
|
|
"""Updates the state of action buttons based on current context."""
|
|
new_folder_state = tk.DISABLED
|
|
new_file_state = tk.DISABLED
|
|
trash_state = tk.DISABLED
|
|
|
|
is_writable = False
|
|
if self.dialog_mode != "open":
|
|
if self.current_fs_type == 'sftp':
|
|
is_writable = True
|
|
else:
|
|
is_writable = os.access(self.current_dir, os.W_OK)
|
|
|
|
if is_writable:
|
|
new_folder_state = tk.NORMAL
|
|
new_file_state = tk.NORMAL
|
|
|
|
if self.dialog_mode == "save":
|
|
trash_state = tk.NORMAL
|
|
|
|
if hasattr(self.widget_manager, 'new_folder_button'):
|
|
self.widget_manager.new_folder_button.config(
|
|
state=new_folder_state)
|
|
|
|
if hasattr(self.widget_manager, 'new_file_button'):
|
|
self.widget_manager.new_file_button.config(state=new_file_state)
|
|
|
|
if hasattr(self.widget_manager, 'trash_button'):
|
|
self.widget_manager.trash_button.config(state=trash_state)
|
|
|
|
def _matches_filetype(self, filename: str) -> bool:
|
|
"""
|
|
Checks if a filename matches the current filetype filter.
|
|
"""
|
|
if self.current_filter_pattern == "*.*":
|
|
return True
|
|
|
|
patterns = self.current_filter_pattern.lower().split()
|
|
fn_lower = filename.lower()
|
|
|
|
for p in patterns:
|
|
if p.startswith('*.'):
|
|
if fn_lower.endswith(p[1:]):
|
|
return True
|
|
elif p.startswith('.'):
|
|
if fn_lower.endswith(p):
|
|
return True
|
|
else:
|
|
if fn_lower == p:
|
|
return True
|
|
return False
|
|
|
|
def _format_size(self, size_bytes: Optional[int]) -> str:
|
|
"""
|
|
Formats a size in bytes into a human-readable string (KB, MB, GB).
|
|
"""
|
|
if size_bytes is None:
|
|
return ""
|
|
if size_bytes < 1024:
|
|
return f"{size_bytes} B"
|
|
if size_bytes < 1024**2:
|
|
return f"{size_bytes/1024:.1f} KB"
|
|
if size_bytes < 1024**3:
|
|
return f"{size_bytes/1024**2:.1f} MB"
|
|
return f"{size_bytes/1024**3:.1f} GB"
|
|
|
|
def shorten_text(self, text: str, max_len: int) -> str:
|
|
"""
|
|
Shortens a string to a maximum length, adding '...' if truncated.
|
|
"""
|
|
return text if len(text) <= max_len else text[:max_len-3] + "..."
|
|
|
|
def _get_mounted_devices(self) -> List[Tuple[str, str, bool]]:
|
|
"""
|
|
Retrieves a list of mounted devices on the system.
|
|
"""
|
|
devices: List[Tuple[str, str, bool]] = []
|
|
root_disk_name: Optional[str] = None
|
|
try:
|
|
result = subprocess.run(['lsblk', '-J', '-o', 'NAME,MOUNTPOINT,FSTYPE,SIZE,RO,RM,TYPE,LABEL,PKNAME'],
|
|
capture_output=True, text=True, check=True)
|
|
data = json.loads(result.stdout)
|
|
|
|
for block_device in data.get('blockdevices', []):
|
|
if 'children' in block_device:
|
|
for child_device in block_device['children']:
|
|
if child_device.get('mountpoint') == '/':
|
|
root_disk_name = block_device.get('name')
|
|
break
|
|
if root_disk_name:
|
|
break
|
|
|
|
for block_device in data.get('blockdevices', []):
|
|
if (
|
|
block_device.get('mountpoint') and
|
|
block_device.get('type') not in ['loop', 'rom'] and
|
|
block_device.get('mountpoint') != '/'):
|
|
|
|
if block_device.get('name').startswith(root_disk_name) and not block_device.get('rm', False):
|
|
pass
|
|
else:
|
|
name = block_device.get('name')
|
|
mountpoint = block_device.get('mountpoint')
|
|
label = block_device.get('label')
|
|
removable = block_device.get('rm', False)
|
|
|
|
display_name = label if label else name
|
|
devices.append((display_name, mountpoint, removable))
|
|
|
|
if 'children' in block_device:
|
|
for child_device in block_device['children']:
|
|
if (
|
|
child_device.get('mountpoint') and
|
|
child_device.get('type') not in ['loop', 'rom'] and
|
|
child_device.get('mountpoint') != '/'):
|
|
|
|
if block_device.get('name') == root_disk_name and not child_device.get('rm', False):
|
|
pass
|
|
else:
|
|
name = child_device.get('name')
|
|
mountpoint = child_device.get('mountpoint')
|
|
label = child_device.get('label')
|
|
removable = child_device.get('rm', False)
|
|
|
|
display_name = label if label else name
|
|
devices.append(
|
|
(display_name, mountpoint, removable))
|
|
|
|
except Exception as e:
|
|
print(f"Error getting mounted devices: {e}")
|
|
return devices
|