add dochstring in cfd_file_operations, cfd_navigation_manager, cfd_search_manager, cfd_settings_dialog and fix entryfield on create new folder and new document in iconview

This commit is contained in:
2025-08-10 11:16:48 +02:00
parent 246addc34b
commit 14a50616a3
10 changed files with 220 additions and 8 deletions

View File

@@ -14,11 +14,28 @@ from cfd_app_config import LocaleStrings, _
class FileOperationsManager:
"""Manages file operations like delete, create, and rename."""
def __init__(self, dialog):
"""
Initializes the FileOperationsManager.
Args:
dialog: The main CustomFileDialog instance.
"""
self.dialog = dialog
def delete_selected_item(self, event=None):
"""Deletes or moves the selected item to trash based on settings."""
"""
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
@@ -61,12 +78,22 @@ class FileOperationsManager:
).show()
def create_new_folder(self):
"""Creates a new folder in the current directory."""
self._create_new_item(is_folder=True)
def create_new_file(self):
"""Creates a new empty file in the current directory."""
self._create_new_item(is_folder=False)
def _create_new_item(self, is_folder):
"""
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)
@@ -82,6 +109,18 @@ class FileOperationsManager:
text=f"{LocaleStrings.FILE['error_creating']}: {e}")
def _get_unique_name(self, base_name):
"""
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
@@ -91,12 +130,28 @@ class FileOperationsManager:
return new_name
def _copy_to_clipboard(self, data):
"""
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, item_path):
"""
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"
@@ -119,6 +174,15 @@ class FileOperationsManager:
return "break"
def _open_file_location_from_context(self, file_path):
"""
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)
@@ -129,6 +193,17 @@ class FileOperationsManager:
self.dialog.after(100, lambda: self.dialog.view_manager._select_file_in_view(filename))
def on_rename_request(self, event, item_path=None, item_frame=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
@@ -141,16 +216,36 @@ class FileOperationsManager:
self.start_rename(item_frame, item_path)
def start_rename(self, item_widget, item_path):
"""
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): The full path to the item being renamed.
"""
if self.dialog.view_mode.get() == "icons":
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, item_path):
"""
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()
@@ -173,7 +268,7 @@ class FileOperationsManager:
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
self.dialog.view_manager.populate_files()
else:
self.dialog.populate_files(item_to_select=os.path.basename(item_path))
self.dialog.view_manager.populate_files(item_to_select=os.path.basename(item_path))
def cancel_rename(event):
self.dialog.view_manager.populate_files()
@@ -183,6 +278,14 @@ class FileOperationsManager:
entry.bind("<Escape>", cancel_rename)
def _start_rename_list_view(self, item_id):
"""
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()
@@ -220,7 +323,7 @@ class FileOperationsManager:
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
self.dialog.view_manager.populate_files()
else:
self.dialog.populate_files(item_to_select=item_text)
self.dialog.view_manager.populate_files(item_to_select=item_text)
entry.destroy()
def cancel_rename(event):

View File

@@ -3,10 +3,27 @@ import tkinter as tk
from cfd_app_config import LocaleStrings, _
class NavigationManager:
"""Manages directory navigation, history, and path handling."""
def __init__(self, dialog):
"""
Initializes the NavigationManager.
Args:
dialog: The main CustomFileDialog instance.
"""
self.dialog = dialog
def handle_path_entry_return(self, event):
"""
Handles the Return key press in the path entry field.
It attempts to navigate to the entered path. If the path is a file,
it navigates to the containing directory and selects the file.
Args:
event: The tkinter event that triggered this handler.
"""
path_text = self.dialog.widget_manager.path_entry.get().strip()
potential_path = os.path.realpath(os.path.expanduser(path_text))
@@ -21,6 +38,18 @@ class NavigationManager:
text=f"{LocaleStrings.CFD['path_not_found']}: {self.dialog.shorten_text(path_text, 50)}")
def navigate_to(self, path, file_to_select=None):
"""
Navigates to a specified directory path.
This is the core navigation method. It validates the path, checks for
read permissions, updates the dialog's current directory, manages the
navigation history, and refreshes the file view.
Args:
path (str): The absolute path to navigate to.
file_to_select (str, optional): If provided, this filename will be
selected after navigation. Defaults to None.
"""
try:
real_path = os.path.realpath(
os.path.abspath(os.path.expanduser(path)))
@@ -49,6 +78,7 @@ class NavigationManager:
self.dialog.widget_manager.search_status_label.config(text=f"{LocaleStrings.CFD['error_title']}: {e}")
def go_back(self):
"""Navigates to the previous directory in the history."""
if self.dialog.history_pos > 0:
self.dialog.history_pos -= 1
self.dialog.current_dir = self.dialog.history[self.dialog.history_pos]
@@ -58,6 +88,7 @@ class NavigationManager:
self.dialog.update_action_buttons_state()
def go_forward(self):
"""Navigates to the next directory in the history."""
if self.dialog.history_pos < len(self.dialog.history) - 1:
self.dialog.history_pos += 1
self.dialog.current_dir = self.dialog.history[self.dialog.history_pos]
@@ -67,11 +98,13 @@ class NavigationManager:
self.dialog.update_action_buttons_state()
def go_up_level(self):
"""Navigates to the parent directory of the current directory."""
new_path = os.path.dirname(self.dialog.current_dir)
if new_path != self.dialog.current_dir:
self.navigate_to(new_path)
def update_nav_buttons(self):
"""Updates the state of the back and forward navigation buttons."""
self.dialog.widget_manager.back_button.config(
state=tk.NORMAL if self.dialog.history_pos > 0 else tk.DISABLED)
self.dialog.widget_manager.forward_button.config(state=tk.NORMAL if self.dialog.history_pos < len(

View File

@@ -9,7 +9,15 @@ from cfd_ui_setup import get_xdg_user_dir
from cfd_app_config import LocaleStrings, _
class SearchManager:
"""Manages the file search functionality, including UI and threading."""
def __init__(self, dialog):
"""
Initializes the SearchManager.
Args:
dialog: The main CustomFileDialog instance.
"""
self.dialog = dialog
def show_search_ready(self, event=None):
@@ -18,7 +26,11 @@ class SearchManager:
self.dialog.widget_manager.search_animation.show_full_circle()
def activate_search(self, event=None):
"""Activates the search entry or cancels an ongoing search."""
"""
Activates the search entry or cancels an ongoing search.
If a search is running, it cancels it. Otherwise, it executes a new search.
"""
if self.dialog.widget_manager.search_animation.running:
if self.dialog.search_thread and self.dialog.search_thread.is_alive():
self.dialog.search_thread.cancelled = True
@@ -28,6 +40,12 @@ class SearchManager:
self.execute_search()
def show_search_bar(self, event=None):
"""
Activates search mode and displays the search bar upon user typing.
Args:
event: The key press event that triggered the search.
"""
if isinstance(event.widget, (ttk.Entry, tk.Entry)) or not event.char.strip():
return
self.dialog.search_mode = True
@@ -37,6 +55,9 @@ class SearchManager:
self.dialog.widget_manager.search_animation.show_full_circle()
def hide_search_bar(self, event=None):
"""
Deactivates search mode, clears the search bar, and restores the file view.
"""
self.dialog.search_mode = False
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
self.dialog.widget_manager.search_status_label.config(text="")
@@ -45,6 +66,11 @@ class SearchManager:
self.dialog.widget_manager.search_animation.hide()
def execute_search(self, event=None):
"""
Initiates a file search in a background thread.
Prevents starting a new search if one is already running.
"""
if self.dialog.search_thread and self.dialog.search_thread.is_alive():
return
search_term = self.dialog.widget_manager.filename_entry.get().strip()
@@ -58,6 +84,16 @@ class SearchManager:
self.dialog.search_thread.start()
def _perform_search_in_thread(self, search_term):
"""
Performs the actual file search in a background thread.
Searches the current directory and relevant XDG user directories.
Handles recursive/non-recursive and hidden/non-hidden file searches.
Updates the UI with results upon completion.
Args:
search_term (str): The term to search for.
"""
self.dialog.search_results.clear()
search_dirs = [self.dialog.current_dir]
home_dir = os.path.expanduser("~")
@@ -144,6 +180,7 @@ class SearchManager:
self.dialog.search_process = None
def show_search_results_treeview(self):
"""Displays the search results in a dedicated Treeview."""
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
widget.destroy()
@@ -244,6 +281,12 @@ class SearchManager:
search_tree.bind("<ButtonRelease-3>", show_context_menu)
def _open_file_location(self, search_tree):
"""
Navigates to the directory of the selected item in the search results.
Args:
search_tree: The Treeview widget containing the search results.
"""
selection = search_tree.selection()
if not selection:
return

View File

@@ -11,7 +11,17 @@ except ImportError:
class SettingsDialog(tk.Toplevel):
"""A dialog window for configuring the application settings."""
def __init__(self, parent, dialog_mode="save"):
"""
Initializes the SettingsDialog.
Args:
parent: The parent widget.
dialog_mode (str, optional): The mode of the main dialog ("open" or "save"),
which affects available settings. Defaults to "save".
"""
super().__init__(parent)
self.transient(parent)
self.grab_set()
@@ -145,6 +155,11 @@ class SettingsDialog(tk.Toplevel):
command=self.destroy).pack(side="right")
def save_settings(self):
"""
Saves the current settings to the configuration file and closes the dialog.
Triggers a UI rebuild in the parent dialog to apply the changes.
"""
new_settings = {
"button_box_pos": self.button_box_pos.get(),
"window_size_preset": self.window_size_preset.get(),
@@ -161,6 +176,7 @@ class SettingsDialog(tk.Toplevel):
self.destroy()
def reset_to_defaults(self):
"""Resets all settings in the dialog to their default values."""
defaults = CfdConfigManager._default_settings
self.button_box_pos.set(defaults["button_box_pos"])
self.window_size_preset.set(defaults["window_size_preset"])

View File

@@ -125,8 +125,16 @@ class ViewManager:
ttk.Label(container_frame, text=error).pack(pady=20)
return
widget_to_focus = self._load_more_items_icon_view(
container_frame, _on_mouse_wheel, item_to_rename, item_to_select)
widget_to_focus = None
while self.dialog.currently_loaded_count < len(self.dialog.all_items):
widget_to_focus = self._load_more_items_icon_view(
container_frame, _on_mouse_wheel, item_to_rename, item_to_select)
if widget_to_focus:
break
if not (item_to_rename or item_to_select):
break
if widget_to_focus:
def scroll_to_widget():
@@ -270,7 +278,12 @@ class ViewManager:
self.dialog.tree.insert("", "end", text=error, values=())
return
self._load_more_items_list_view(item_to_rename, item_to_select)
while self.dialog.currently_loaded_count < len(self.dialog.all_items):
item_found = self._load_more_items_list_view(item_to_rename, item_to_select)
if item_found:
break
if not (item_to_rename or item_to_select):
break
def _load_more_items_list_view(self, item_to_rename=None, item_to_select=None):
start_index = self.dialog.currently_loaded_count
@@ -278,8 +291,9 @@ class ViewManager:
self.dialog.items_to_load_per_batch)
if start_index >= end_index:
return
return False
item_found = False
for i in range(start_index, end_index):
name = self.dialog.all_items[i]
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
@@ -305,14 +319,17 @@ class ViewManager:
self.dialog.tree.focus(item_id)
self.dialog.tree.see(item_id)
self.dialog.file_op_manager.start_rename(item_id, path)
item_found = True
elif name == item_to_select:
self.dialog.tree.selection_set(item_id)
self.dialog.tree.focus(item_id)
self.dialog.tree.see(item_id)
item_found = True
except (FileNotFoundError, PermissionError):
continue
self.dialog.currently_loaded_count = end_index
return item_found
def on_item_select(self, path, item_frame):
if hasattr(self.dialog, 'selected_item_frame') and self.dialog.selected_item_frame.winfo_exists():