Files
shared_libs/custom_file_dialog/cfd_file_operations.py

343 lines
13 KiB
Python

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.selected_file or not os.path.exists(self.dialog.selected_file):
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(self.dialog.selected_file)
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(self.dialog.selected_file)
else:
if os.path.isdir(self.dialog.selected_file):
shutil.rmtree(self.dialog.selected_file)
else:
os.remove(self.dialog.selected_file)
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 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
while os.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("<Return>", finish_rename)
entry.bind("<FocusOut>", finish_rename)
entry.bind("<Escape>", 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("<Return>", finish_rename)
entry.bind("<FocusOut>", finish_rename)
entry.bind("<Escape>", 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 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()