import os import shutil import tkinter as tk from tkinter import ttk from typing import Optional, Any, TYPE_CHECKING try: import send2trash SEND2TRASH_AVAILABLE = True except ImportError: SEND2TRASH_AVAILABLE = False from shared_libs.message import MessageDialog from .cfd_app_config import LocaleStrings if TYPE_CHECKING: from custom_file_dialog import CustomFileDialog class FileOperationsManager: """Manages file operations like delete, create, and rename.""" def __init__(self, dialog: 'CustomFileDialog') -> None: """ Initializes the FileOperationsManager. Args: dialog: The main CustomFileDialog instance. """ self.dialog = dialog def delete_selected_item(self, event: Optional[tk.Event] = None) -> None: """ Deletes the selected item or moves it to the trash. This method checks user settings to determine whether to move the item to the system's trash (if available) or delete it permanently. It also handles the confirmation dialog based on user preferences. Args: event: The event that triggered the deletion (optional). """ if not self.dialog.result or not isinstance(self.dialog.result, str): return selected_path = self.dialog.result if self.dialog.current_fs_type == 'sftp': item_name = os.path.basename(selected_path) dialog = MessageDialog( master=self.dialog, title=LocaleStrings.FILE["confirm_delete_title"], text=f"{LocaleStrings.FILE['are_you_sure']} '{item_name}' {LocaleStrings.FILE['delete_permanently']}?", message_type="question" ) if not dialog.show(): return try: if self.dialog.sftp_manager.path_is_dir(selected_path): success, msg = self.dialog.sftp_manager.rm_recursive(selected_path) else: success, msg = self.dialog.sftp_manager.rm(selected_path) if not success: raise OSError(msg) self.dialog.view_manager.populate_files() self.dialog.widget_manager.search_status_label.config( text=f"'{item_name}' {LocaleStrings.FILE['was_successfully_removed']}") except Exception as e: MessageDialog( master=self.dialog, title=LocaleStrings.FILE["error_title"], text=f"{LocaleStrings.FILE['error_removing']} '{item_name}':\n{e}", message_type="error" ).show() return # Local deletion logic if not os.path.exists(selected_path): return use_trash = self.dialog.settings.get( "use_trash", False) and SEND2TRASH_AVAILABLE confirm = self.dialog.settings.get("confirm_delete", False) action_text = LocaleStrings.FILE["move_to_trash"] if use_trash else LocaleStrings.FILE["delete_permanently"] item_name = os.path.basename(selected_path) if not confirm: dialog = MessageDialog( master=self.dialog, title=LocaleStrings.FILE["confirm_delete_title"], text=f"{LocaleStrings.FILE['are_you_sure']} '{item_name}' {action_text}?", message_type="question" ) if not dialog.show(): return try: if use_trash: send2trash.send2trash(selected_path) else: if os.path.isdir(selected_path): shutil.rmtree(selected_path) else: os.remove(selected_path) self.dialog.view_manager.populate_files() self.dialog.widget_manager.search_status_label.config( text=f"'{item_name}' {LocaleStrings.FILE['was_successfully_removed']}") except Exception as e: MessageDialog( master=self.dialog, title=LocaleStrings.FILE["error_title"], text=f"{LocaleStrings.FILE['error_removing']} '{item_name}':\n{e}", message_type="error" ).show() def create_new_folder(self) -> None: """Creates a new folder in the current directory.""" self._create_new_item(is_folder=True) def create_new_file(self) -> None: """Creates a new empty file in the current directory.""" self._create_new_item(is_folder=False) def _create_new_item(self, is_folder: bool) -> None: """ Internal helper to create a new file or folder. It generates a unique name and creates the item, then refreshes the view. Args: is_folder (bool): True to create a folder, False to create a file. """ base_name = LocaleStrings.FILE["new_folder_title"] if is_folder else LocaleStrings.FILE["new_document_txt"] new_name = self._get_unique_name(base_name) new_path = os.path.join(self.dialog.current_dir, new_name) try: if self.dialog.current_fs_type == 'sftp': if is_folder: success, msg = self.dialog.sftp_manager.mkdir(new_path) if not success: raise OSError(msg) else: success, msg = self.dialog.sftp_manager.touch(new_path) if not success: raise OSError(msg) else: if is_folder: os.mkdir(new_path) else: open(new_path, 'a').close() self.dialog.view_manager.populate_files(item_to_rename=new_name) except Exception as e: self.dialog.widget_manager.search_status_label.config( text=f"{LocaleStrings.FILE['error_creating']}: {e}") def _get_unique_name(self, base_name: str) -> str: """ Generates a unique name for a file or folder. If a file or folder with `base_name` already exists, it appends a counter (e.g., "New Folder 2") until a unique name is found. Args: base_name (str): The initial name for the item. Returns: str: A unique name for the item in the current directory. """ name, ext = os.path.splitext(base_name) counter = 1 new_name = base_name path_exists = self.dialog.sftp_manager.exists if self.dialog.current_fs_type == 'sftp' else os.path.exists while path_exists(os.path.join(self.dialog.current_dir, new_name)): counter += 1 new_name = f"{name} {counter}{ext}" return new_name def _copy_to_clipboard(self, data: str) -> None: """ Copies the given data to the system clipboard. Args: data (str): The text to be copied. """ self.dialog.clipboard_clear() self.dialog.clipboard_append(data) self.dialog.widget_manager.search_status_label.config( text=f"'{self.dialog.shorten_text(data, 50)}' {LocaleStrings.FILE['copied_to_clipboard']}") def _show_context_menu(self, event: tk.Event, item_path: str) -> str: """ Displays a context menu for the selected item. Args: event: The mouse event that triggered the menu. item_path (str): The full path to the item. Returns: str: "break" to prevent further event propagation. """ if not item_path: return "break" if hasattr(self.dialog, 'context_menu') and self.dialog.context_menu.winfo_exists(): self.dialog.context_menu.destroy() self.dialog.context_menu = tk.Menu(self.dialog, tearoff=0, background=self.dialog.style_manager.header, foreground=self.dialog.style_manager.color_foreground, activebackground=self.dialog.style_manager.selection_color, activeforeground=self.dialog.style_manager.color_foreground, relief='flat', borderwidth=0) self.dialog.context_menu.add_command(label=LocaleStrings.UI["copy_filename_to_clipboard"], command=lambda: self._copy_to_clipboard(os.path.basename(item_path)), image=self.dialog.icon_manager.get_icon('copy'), compound='left') self.dialog.context_menu.add_command( label=LocaleStrings.UI["copy_path_to_clipboard"], command=lambda: self._copy_to_clipboard(item_path), image=self.dialog.icon_manager.get_icon('copy'), compound='left') self.dialog.context_menu.add_separator() self.dialog.context_menu.add_command( label=LocaleStrings.UI["open_file_location"], command=lambda: self._open_file_location_from_context(item_path), image=self.dialog.icon_manager.get_icon('stair'), compound='left') self.dialog.context_menu.tk_popup(event.x_root, event.y_root) return "break" def _open_file_location_from_context(self, file_path: str) -> None: """ Navigates to the location of the given file path. This is used by the context menu to jump to a file's directory, which is especially useful when in search mode. Args: file_path (str): The full path to the file. """ directory = os.path.dirname(file_path) filename = os.path.basename(file_path) if self.dialog.search_mode: self.dialog.search_manager.hide_search_bar() self.dialog.navigation_manager.navigate_to(directory) self.dialog.after( 100, lambda: self.dialog.view_manager._select_file_in_view(filename)) def on_rename_request(self, event: tk.Event, item_path: Optional[str] = None, item_frame: Optional[tk.Widget] = None) -> None: """ Handles the initial request to rename an item. This method is triggered by an event (e.g., F2 key press) and initiates the renaming process based on the current view mode. Args: event: The event that triggered the rename. item_path (str, optional): The path of the item in icon view. item_frame (tk.Widget, optional): The frame of the item in icon view. """ if self.dialog.view_mode.get() == "list": if not self.dialog.tree.selection(): return item_id = self.dialog.tree.selection()[0] self.start_rename(item_id) else: # icon view if item_path and item_frame: self.start_rename(item_frame, item_path) def start_rename(self, item_widget: Any, item_path: Optional[str] = None) -> None: """ Starts the renaming UI for an item. Dispatches to the appropriate method based on the current view mode. Args: item_widget: The widget representing the item (item_id for list view, item_frame for icon view). item_path (str, optional): The full path to the item being renamed. Required for icon view. """ if self.dialog.view_mode.get() == "icons": if item_path: self._start_rename_icon_view(item_widget, item_path) else: # list view self._start_rename_list_view(item_widget) # item_widget is item_id def _start_rename_icon_view(self, item_frame: ttk.Frame, item_path: str) -> None: """ Initiates the in-place rename UI for an item in icon view. It replaces the item's label with an Entry widget. Args: item_frame (tk.Widget): The frame containing the item's icon and label. item_path (str): The full path to the item. """ for child in item_frame.winfo_children(): child.destroy() entry = ttk.Entry(item_frame) entry.pack(fill="both", expand=True, padx=2, pady=20) entry.insert(0, os.path.basename(item_path)) entry.select_range(0, tk.END) entry.focus_set() def finish_rename(event: tk.Event) -> None: new_name = entry.get() self._finish_rename_logic(item_path, new_name) def cancel_rename(event: tk.Event) -> None: self.dialog.view_manager.populate_files() entry.bind("", finish_rename) entry.bind("", finish_rename) entry.bind("", cancel_rename) def _start_rename_list_view(self, item_id: str) -> None: """ Initiates the in-place rename UI for an item in list view. It places an Entry widget over the Treeview item's cell. Args: item_id: The ID of the treeview item to be renamed. """ self.dialog.tree.see(item_id) self.dialog.tree.update_idletasks() bbox = self.dialog.tree.bbox(item_id, column="#0") if not bbox: return x, y, width, height = bbox entry = ttk.Entry(self.dialog.tree) entry_width = self.dialog.tree.column("#0", "width") entry.place(x=x, y=y, width=entry_width, height=height) item_text = self.dialog.tree.item(item_id, "text").strip() entry.insert(0, item_text) entry.select_range(0, tk.END) entry.focus_set() old_path = os.path.join(self.dialog.current_dir, item_text) def finish_rename(event: tk.Event) -> None: new_name = entry.get() entry.destroy() self._finish_rename_logic(old_path, new_name) def cancel_rename(event: tk.Event) -> None: entry.destroy() entry.bind("", finish_rename) entry.bind("", finish_rename) entry.bind("", cancel_rename) def _finish_rename_logic(self, old_path: str, new_name: str) -> None: """ Handles the core logic of renaming a file or folder after the user submits the new name from an Entry widget. Args: old_path (str): The original full path of the item. new_name (str): The new name for the item. """ new_path = os.path.join(self.dialog.current_dir, new_name) old_name = os.path.basename(old_path) if not new_name or new_path == old_path: self.dialog.view_manager.populate_files(item_to_select=old_name) return if self.dialog.current_fs_type == 'sftp': if self.dialog.sftp_manager.exists(new_path): self.dialog.widget_manager.search_status_label.config( text=f"'{new_name}' {LocaleStrings.FILE['folder_exists_error']}") self.dialog.view_manager.populate_files(item_to_select=old_name) return try: success, msg = self.dialog.sftp_manager.rename(old_path, new_path) if not success: raise OSError(msg) self.dialog.view_manager.populate_files(item_to_select=new_name) except Exception as e: self.dialog.widget_manager.search_status_label.config( text=f"{LocaleStrings.FILE['error_renaming']}: {e}") self.dialog.view_manager.populate_files() else: if os.path.exists(new_path): self.dialog.widget_manager.search_status_label.config( text=f"'{new_name}' {LocaleStrings.FILE['folder_exists_error']}") self.dialog.view_manager.populate_files(item_to_select=old_name) return try: os.rename(old_path, new_path) self.dialog.view_manager.populate_files(item_to_select=new_name) except Exception as e: self.dialog.widget_manager.search_status_label.config( text=f"{LocaleStrings.FILE['error_renaming']}: {e}") self.dialog.view_manager.populate_files()