add doctring and typhint in custom_file_dialog and add type-hints on cfd_view_manager, cfd_ui_setup, cfd_settings_dialog, cfd_search_manager, cfd_navigation_manager

This commit is contained in:
2025-08-10 11:57:43 +02:00
parent 4e7ef8d348
commit 5a41d6b1fd
6 changed files with 425 additions and 113 deletions

View File

@@ -1,11 +1,17 @@
import os
import tkinter as tk
from typing import Optional, TYPE_CHECKING
from cfd_app_config import LocaleStrings, _
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
class NavigationManager:
"""Manages directory navigation, history, and path handling."""
def __init__(self, dialog):
def __init__(self, dialog: 'CustomFileDialog') -> None:
"""
Initializes the NavigationManager.
@@ -14,7 +20,7 @@ class NavigationManager:
"""
self.dialog = dialog
def handle_path_entry_return(self, event):
def handle_path_entry_return(self, event: tk.Event) -> None:
"""
Handles the Return key press in the path entry field.
@@ -37,7 +43,7 @@ class NavigationManager:
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.CFD['path_not_found']}: {self.dialog.shorten_text(path_text, 50)}")
def navigate_to(self, path, file_to_select=None):
def navigate_to(self, path: str, file_to_select: Optional[str] = None) -> None:
"""
Navigates to a specified directory path.
@@ -77,7 +83,7 @@ class NavigationManager:
except Exception as e:
self.dialog.widget_manager.search_status_label.config(text=f"{LocaleStrings.CFD['error_title']}: {e}")
def go_back(self):
def go_back(self) -> None:
"""Navigates to the previous directory in the history."""
if self.dialog.history_pos > 0:
self.dialog.history_pos -= 1
@@ -87,7 +93,7 @@ class NavigationManager:
self.dialog.update_status_bar()
self.dialog.update_action_buttons_state()
def go_forward(self):
def go_forward(self) -> None:
"""Navigates to the next directory in the history."""
if self.dialog.history_pos < len(self.dialog.history) - 1:
self.dialog.history_pos += 1
@@ -97,13 +103,13 @@ class NavigationManager:
self.dialog.update_status_bar()
self.dialog.update_action_buttons_state()
def go_up_level(self):
def go_up_level(self) -> None:
"""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):
def update_nav_buttons(self) -> None:
"""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)

View File

@@ -4,14 +4,20 @@ import subprocess
from datetime import datetime
import tkinter as tk
from tkinter import ttk
from typing import Optional, TYPE_CHECKING
from shared_libs.message import MessageDialog
from cfd_ui_setup import get_xdg_user_dir
from cfd_app_config import LocaleStrings, _
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
class SearchManager:
"""Manages the file search functionality, including UI and threading."""
def __init__(self, dialog):
def __init__(self, dialog: 'CustomFileDialog') -> None:
"""
Initializes the SearchManager.
@@ -20,12 +26,12 @@ class SearchManager:
"""
self.dialog = dialog
def show_search_ready(self, event=None):
def show_search_ready(self, event: Optional[tk.Event] = None) -> None:
"""Shows the static 'full circle' to indicate search is ready."""
if not self.dialog.search_mode:
self.dialog.widget_manager.search_animation.show_full_circle()
def activate_search(self, event=None):
def activate_search(self, event: Optional[tk.Event] = None) -> None:
"""
Activates the search entry or cancels an ongoing search.
@@ -39,7 +45,7 @@ class SearchManager:
else:
self.execute_search()
def show_search_bar(self, event=None):
def show_search_bar(self, event: tk.Event) -> None:
"""
Activates search mode and displays the search bar upon user typing.
@@ -54,7 +60,7 @@ class SearchManager:
self.dialog.widget_manager.filename_entry.insert(0, event.char)
self.dialog.widget_manager.search_animation.show_full_circle()
def hide_search_bar(self, event=None):
def hide_search_bar(self, event: Optional[tk.Event] = None) -> None:
"""
Deactivates search mode, clears the search bar, and restores the file view.
"""
@@ -65,7 +71,7 @@ class SearchManager:
self.dialog.view_manager.populate_files()
self.dialog.widget_manager.search_animation.hide()
def execute_search(self, event=None):
def execute_search(self, event: Optional[tk.Event] = None) -> None:
"""
Initiates a file search in a background thread.
@@ -83,7 +89,7 @@ class SearchManager:
self.dialog.search_thread = threading.Thread(target=self._perform_search_in_thread, args=(search_term,))
self.dialog.search_thread.start()
def _perform_search_in_thread(self, search_term):
def _perform_search_in_thread(self, search_term: str) -> None:
"""
Performs the actual file search in a background thread.
@@ -157,7 +163,7 @@ class SearchManager:
seen = set()
self.dialog.search_results = [x for x in all_files if not (x in seen or seen.add(x))]
def update_ui():
def update_ui() -> None:
if self.dialog.search_results:
self.show_search_results_treeview()
folder_count = sum(1 for p in self.dialog.search_results if os.path.isdir(p))
@@ -179,7 +185,7 @@ class SearchManager:
self.dialog.after(0, self.dialog.widget_manager.search_animation.stop)
self.dialog.search_process = None
def show_search_results_treeview(self):
def show_search_results_treeview(self) -> None:
"""Displays the search results in a dedicated Treeview."""
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
widget.destroy()
@@ -232,7 +238,7 @@ class SearchManager:
except (FileNotFoundError, PermissionError):
continue
def on_search_select(event):
def on_search_select(event: tk.Event) -> None:
selection = search_tree.selection()
if selection:
item = search_tree.item(selection[0])
@@ -254,7 +260,7 @@ class SearchManager:
search_tree.bind("<<TreeviewSelect>>", on_search_select)
def on_search_double_click(event):
def on_search_double_click(event: tk.Event) -> None:
selection = search_tree.selection()
if selection:
item = search_tree.item(selection[0])
@@ -266,7 +272,7 @@ class SearchManager:
search_tree.bind("<Double-1>", on_search_double_click)
def show_context_menu(event):
def show_context_menu(event: tk.Event) -> str:
iid = search_tree.identify_row(event.y)
if not iid:
return "break"
@@ -280,7 +286,7 @@ class SearchManager:
search_tree.bind("<ButtonRelease-3>", show_context_menu)
def _open_file_location(self, search_tree):
def _open_file_location(self, search_tree: ttk.Treeview) -> None:
"""
Navigates to the directory of the selected item in the search results.

View File

@@ -1,8 +1,13 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from cfd_app_config import CfdConfigManager, LocaleStrings, _
from animated_icon import PIL_AVAILABLE
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
try:
import send2trash
SEND2TRASH_AVAILABLE = True
@@ -13,7 +18,7 @@ except ImportError:
class SettingsDialog(tk.Toplevel):
"""A dialog window for configuring the application settings."""
def __init__(self, parent, dialog_mode="save"):
def __init__(self, parent: 'CustomFileDialog', dialog_mode: str = "save") -> None:
"""
Initializes the SettingsDialog.
@@ -154,7 +159,7 @@ class SettingsDialog(tk.Toplevel):
ttk.Button(button_frame, text=LocaleStrings.SET["cancel_button"],
command=self.destroy).pack(side="right")
def save_settings(self):
def save_settings(self) -> None:
"""
Saves the current settings to the configuration file and closes the dialog.
@@ -175,7 +180,7 @@ class SettingsDialog(tk.Toplevel):
self.master.reload_config_and_rebuild_ui()
self.destroy()
def reset_to_defaults(self):
def reset_to_defaults(self) -> None:
"""Resets all settings in the dialog to their default values."""
defaults = CfdConfigManager._default_settings
self.button_box_pos.set(defaults["button_box_pos"])

View File

@@ -2,12 +2,19 @@ import os
import shutil
import tkinter as tk
from tkinter import ttk
from typing import Dict, Any
# To avoid circular import with custom_file_dialog.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
from shared_libs.common_tools import Tooltip
from animated_icon import AnimatedIcon
from cfd_app_config import LocaleStrings, _
def get_xdg_user_dir(dir_key, fallback_name):
def get_xdg_user_dir(dir_key: str, fallback_name: str) -> str:
"""
Retrieves a user directory path from the XDG user-dirs.dirs config file.
@@ -43,7 +50,7 @@ def get_xdg_user_dir(dir_key, fallback_name):
class StyleManager:
"""Manages the visual styling of the application using ttk styles."""
def __init__(self, dialog):
def __init__(self, dialog: 'CustomFileDialog') -> None:
"""
Initializes the StyleManager.
@@ -53,7 +60,7 @@ class StyleManager:
self.dialog = dialog
self.setup_styles()
def setup_styles(self):
def setup_styles(self) -> None:
"""
Configures all the ttk styles for the dialog based on a light or dark theme.
"""
@@ -133,7 +140,7 @@ class StyleManager:
class WidgetManager:
"""Manages the creation, layout, and management of all widgets in the dialog."""
def __init__(self, dialog, settings):
def __init__(self, dialog: 'CustomFileDialog', settings: Dict[str, Any]) -> None:
"""
Initializes the WidgetManager.
@@ -146,7 +153,7 @@ class WidgetManager:
self.settings = settings
self.setup_widgets()
def _setup_top_bar(self, parent_frame):
def _setup_top_bar(self, parent_frame: ttk.Frame) -> None:
"""Sets up the top bar with navigation and control buttons."""
top_bar = ttk.Frame(parent_frame, style='Accent.TFrame', padding=(0, 5, 0, 5))
top_bar.grid(row=0, column=0, columnspan=2, sticky="ew")
@@ -231,7 +238,7 @@ class WidgetManager:
self.more_button = ttk.Button(right_controls_container, text="...",
command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3)
def _setup_sidebar(self, parent_paned_window):
def _setup_sidebar(self, parent_paned_window: ttk.PanedWindow) -> None:
"""Sets up the sidebar with bookmarks and devices."""
sidebar_frame = ttk.Frame(parent_paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200)
sidebar_frame.grid_propagate(False)
@@ -250,7 +257,7 @@ class WidgetManager:
self._setup_sidebar_storage(sidebar_frame)
def _setup_sidebar_bookmarks(self, sidebar_frame):
def _setup_sidebar_bookmarks(self, sidebar_frame: ttk.Frame) -> None:
"""Sets up the bookmark buttons in the sidebar."""
sidebar_buttons_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0))
sidebar_buttons_frame.grid(row=0, column=0, sticky="nsew")
@@ -269,7 +276,7 @@ class WidgetManager:
btn.pack(fill="x", pady=1)
self.sidebar_buttons.append((btn, f" {config['name']}"))
def _setup_sidebar_devices(self, sidebar_frame):
def _setup_sidebar_devices(self, sidebar_frame: ttk.Frame) -> None:
"""Sets up the mounted devices section in the sidebar."""
mounted_devices_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10)
@@ -298,7 +305,7 @@ class WidgetManager:
self.devices_scrollable_frame.bind(
"<Leave>", self.dialog._on_devices_leave)
def _configure_devices_canvas(event):
def _configure_devices_canvas(event: tk.Event) -> None:
self.devices_canvas.configure(
scrollregion=self.devices_canvas.bbox("all"))
canvas_width = event.width
@@ -309,7 +316,7 @@ class WidgetManager:
scrollregion=self.devices_canvas.bbox("all")))
self.devices_canvas.bind("<Configure>", _configure_devices_canvas)
def _on_devices_mouse_wheel(event):
def _on_devices_mouse_wheel(event: tk.Event) -> None:
if event.num == 4:
delta = -1
elif event.num == 5:
@@ -358,7 +365,7 @@ class WidgetManager:
except (FileNotFoundError, PermissionError):
pass
def _setup_sidebar_storage(self, sidebar_frame):
def _setup_sidebar_storage(self, sidebar_frame: ttk.Frame) -> None:
"""Sets up the storage indicator in the sidebar."""
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
storage_frame.grid(row=5, column=0, sticky="sew", padx=10, pady=10)
@@ -367,7 +374,7 @@ class WidgetManager:
self.storage_bar = ttk.Progressbar(storage_frame, orient="horizontal", length=100, mode="determinate")
self.storage_bar.pack(fill="x", pady=(2, 5), padx=15)
def _setup_bottom_bar(self):
def _setup_bottom_bar(self) -> None:
"""Sets up the bottom bar including filename entry, action buttons, and status info."""
self.action_status_frame = ttk.Frame(self.content_frame, style="AccentBottom.TFrame")
self.action_status_frame.grid(row=1, column=0, sticky="ew", pady=(5, 5), padx=10)
@@ -429,7 +436,7 @@ class WidgetManager:
self._layout_bottom_buttons(button_box_pos)
def _layout_bottom_buttons(self, button_box_pos):
def _layout_bottom_buttons(self, button_box_pos: str) -> None:
"""Lays out the bottom action buttons based on user settings."""
# Configure container weights
self.left_container.grid_rowconfigure(0, weight=1)
@@ -463,7 +470,7 @@ class WidgetManager:
self.filter_combobox.grid(in_=self.center_container, row=1, column=1, sticky="e", pady=(5, 0))
def setup_widgets(self):
def setup_widgets(self) -> None:
"""Creates and arranges all widgets in the main dialog window."""
# Main container
main_frame = ttk.Frame(self.dialog, style='Accent.TFrame')

View File

@@ -2,12 +2,19 @@ import os
import tkinter as tk
from tkinter import ttk
from datetime import datetime
from typing import Optional, List, Tuple, Callable, Any
# To avoid circular import with custom_file_dialog.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
from shared_libs.common_tools import Tooltip
from cfd_app_config import AppConfig, LocaleStrings, _
class ViewManager:
"""Manages the display of files and folders in list and icon views."""
def __init__(self, dialog):
def __init__(self, dialog: 'CustomFileDialog'):
"""
Initializes the ViewManager.
@@ -16,7 +23,7 @@ class ViewManager:
"""
self.dialog = dialog
def populate_files(self, item_to_rename=None, item_to_select=None):
def populate_files(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
"""
Populates the main file display area.
@@ -42,7 +49,7 @@ class ViewManager:
else:
self.populate_icon_view(item_to_rename, item_to_select)
def _get_sorted_items(self):
def _get_sorted_items(self) -> Tuple[List[str], Optional[str], Optional[str]]:
"""
Gets a sorted list of items from the current directory.
@@ -66,7 +73,7 @@ class ViewManager:
except FileNotFoundError:
return ([], LocaleStrings.CFD["directory_not_found"], None)
def _get_folder_content_count(self, folder_path):
def _get_folder_content_count(self, folder_path: str) -> Optional[int]:
"""
Counts the number of items in a given folder.
@@ -89,7 +96,7 @@ class ViewManager:
except (PermissionError, FileNotFoundError):
return None
def _get_item_path_from_widget(self, widget):
def _get_item_path_from_widget(self, widget: tk.Widget) -> Optional[str]:
"""
Traverses up the widget hierarchy to find the item_path attribute.
@@ -103,7 +110,7 @@ class ViewManager:
widget = widget.master
return getattr(widget, 'item_path', None)
def _handle_icon_click(self, event):
def _handle_icon_click(self, event: tk.Event) -> None:
"""Handles a single click on an icon view item."""
item_path = self._get_item_path_from_widget(event.widget)
if item_path:
@@ -112,19 +119,19 @@ class ViewManager:
item_frame = item_frame.master
self.on_item_select(item_path, item_frame)
def _handle_icon_double_click(self, event):
def _handle_icon_double_click(self, event: tk.Event) -> None:
"""Handles a double click on an icon view item."""
item_path = self._get_item_path_from_widget(event.widget)
if item_path:
self.on_item_double_click(item_path)
def _handle_icon_context_menu(self, event):
def _handle_icon_context_menu(self, event: tk.Event) -> None:
"""Handles a context menu request on an icon view item."""
item_path = self._get_item_path_from_widget(event.widget)
if item_path:
self.dialog.file_op_manager._show_context_menu(event, item_path)
def _handle_icon_rename_request(self, event):
def _handle_icon_rename_request(self, event: tk.Event) -> None:
"""Handles a rename request on an icon view item."""
item_path = self._get_item_path_from_widget(event.widget)
if item_path:
@@ -133,7 +140,7 @@ class ViewManager:
item_frame = item_frame.master
self.dialog.file_op_manager.on_rename_request(event, item_path, item_frame)
def populate_icon_view(self, item_to_rename=None, item_to_select=None):
def populate_icon_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
"""
Populates the file display with items in an icon grid layout.
@@ -157,7 +164,7 @@ class ViewManager:
container_frame.bind("<Configure>", lambda e: self.dialog.icon_canvas.configure(
scrollregion=self.dialog.icon_canvas.bbox("all")))
def _on_mouse_wheel(event):
def _on_mouse_wheel(event: tk.Event) -> None:
if event.num == 4:
delta = -1
elif event.num == 5:
@@ -191,7 +198,7 @@ class ViewManager:
break
if widget_to_focus:
def scroll_to_widget():
def scroll_to_widget() -> None:
self.dialog.update_idletasks()
if not widget_to_focus.winfo_exists():
return
@@ -207,7 +214,7 @@ class ViewManager:
self.dialog.after(100, scroll_to_widget)
def _load_more_items_icon_view(self, container, scroll_handler, item_to_rename=None, item_to_select=None):
def _load_more_items_icon_view(self, container: ttk.Frame, scroll_handler: Callable, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> Optional[ttk.Frame]:
"""
Loads a batch of items into the icon view.
@@ -293,7 +300,7 @@ class ViewManager:
self.dialog.currently_loaded_count = end_index
return widget_to_focus
def populate_list_view(self, item_to_rename=None, item_to_select=None):
def populate_list_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
"""
Populates the file display with items in a list (Treeview) layout.
@@ -334,7 +341,7 @@ class ViewManager:
v_scrollbar.grid(row=0, column=1, sticky='ns')
h_scrollbar.grid(row=1, column=0, sticky='ew')
def _on_scroll(*args):
def _on_scroll(*args: Any) -> None:
if self.dialog.currently_loaded_count < len(self.dialog.all_items) and self.dialog.tree.yview()[1] > 0.9:
self._load_more_items_list_view()
v_scrollbar.set(*args)
@@ -358,7 +365,7 @@ class ViewManager:
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):
def _load_more_items_list_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> bool:
"""
Loads a batch of items into the list view.
@@ -414,7 +421,7 @@ class ViewManager:
self.dialog.currently_loaded_count = end_index
return item_found
def on_item_select(self, path, item_frame):
def on_item_select(self, path: str, item_frame: ttk.Frame) -> None:
"""
Handles the selection of an item in the icon view.
@@ -439,7 +446,7 @@ class ViewManager:
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
self.dialog.widget_manager.filename_entry.insert(0, os.path.basename(path))
def on_list_select(self, event):
def on_list_select(self, event: tk.Event) -> None:
"""Handles the selection of an item in the list view."""
if not self.dialog.tree.selection():
return
@@ -453,7 +460,7 @@ class ViewManager:
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
self.dialog.widget_manager.filename_entry.insert(0, item_text)
def on_list_context_menu(self, event):
def on_list_context_menu(self, event: tk.Event) -> str:
"""Shows the context menu for a list view item."""
iid = self.dialog.tree.identify_row(event.y)
if not iid:
@@ -464,7 +471,7 @@ class ViewManager:
self.dialog.file_op_manager._show_context_menu(event, item_path)
return "break"
def on_item_double_click(self, path):
def on_item_double_click(self, path: str) -> None:
"""
Handles a double-click on an icon view item.
@@ -482,7 +489,7 @@ class ViewManager:
0, os.path.basename(path))
self.dialog.on_save()
def on_list_double_click(self, event):
def on_list_double_click(self, event: tk.Event) -> None:
"""Handles a double-click on a list view item."""
if not self.dialog.tree.selection():
return
@@ -499,7 +506,7 @@ class ViewManager:
self.dialog.widget_manager.filename_entry.insert(0, item_text)
self.dialog.on_save()
def _select_file_in_view(self, filename):
def _select_file_in_view(self, filename: str) -> None:
"""
Programmatically selects a file in the current view.
@@ -524,7 +531,7 @@ class ViewManager:
if hasattr(widget, 'item_path') and widget.item_path == target_path:
self.on_item_select(widget.item_path, widget)
def scroll_to_widget():
def scroll_to_widget() -> None:
self.dialog.update_idletasks()
if not widget.winfo_exists(): return
y = widget.winfo_y()
@@ -540,7 +547,7 @@ class ViewManager:
self.dialog.after(100, scroll_to_widget)
break
def _update_view_mode_buttons(self):
def _update_view_mode_buttons(self) -> None:
"""Updates the visual state of the view mode toggle buttons."""
if self.dialog.view_mode.get() == "icons":
self.dialog.widget_manager.icon_view_button.configure(
@@ -553,19 +560,19 @@ class ViewManager:
self.dialog.widget_manager.icon_view_button.configure(
style="Header.TButton.Borderless.Round")
def set_icon_view(self):
def set_icon_view(self) -> None:
"""Switches to icon view and repopulates the files."""
self.dialog.view_mode.set("icons")
self._update_view_mode_buttons()
self.populate_files()
def set_list_view(self):
def set_list_view(self) -> None:
"""Switches to list view and repopulates the files."""
self.dialog.view_mode.set("list")
self._update_view_mode_buttons()
self.populate_files()
def toggle_hidden_files(self):
def toggle_hidden_files(self) -> None:
"""Toggles the visibility of hidden files and refreshes the view."""
self.dialog.show_hidden_files.set(not self.dialog.show_hidden_files.get())
if self.dialog.show_hidden_files.get():
@@ -580,7 +587,7 @@ class ViewManager:
LocaleStrings.UI["show_hidden_files"])
self.populate_files()
def on_filter_change(self, event):
def on_filter_change(self, event: tk.Event) -> None:
"""Handles a change in the file type filter combobox."""
selected_desc = self.dialog.widget_manager.filter_combobox.get()
for desc, pattern in self.dialog.filetypes:
@@ -589,7 +596,7 @@ class ViewManager:
break
self.populate_files()
def _unbind_mouse_wheel_events(self):
def _unbind_mouse_wheel_events(self) -> None:
"""Unbinds all mouse wheel events from the dialog."""
self.dialog.unbind_all("<MouseWheel>")
self.dialog.unbind_all("<Button-4>")

View File

@@ -6,6 +6,8 @@ from datetime import datetime
import subprocess
import json
import threading
from typing import Optional, List, Tuple, Any, Dict
from shared_libs.message import MessageDialog
from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools
from cfd_app_config import AppConfig, CfdConfigManager, LocaleStrings, _
@@ -18,11 +20,27 @@ from cfd_navigation_manager import NavigationManager
from cfd_view_manager import ViewManager
class CustomFileDialog(tk.Toplevel):
def __init__(self, parent, initial_dir=None, filetypes=None, dialog_mode="open", title=LocaleStrings.CFD["title"]):
"""
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,
dialog_mode: str = "open", title: str = LocaleStrings.CFD["title"]):
"""
Initializes the CustomFileDialog.
Args:
parent: The parent widget.
initial_dir: The initial directory to display.
filetypes: A list of filetype tuples, e.g., [('Text files', '*.txt')].
dialog_mode: The dialog mode, either "open" or "save".
title: The title of the dialog window.
"""
super().__init__(parent)
self.my_tool_tip = None
self.dialog_mode = dialog_mode
self.my_tool_tip: Optional[Tooltip] = None
self.dialog_mode: str = dialog_mode
self.load_settings()
@@ -32,45 +50,45 @@ class CustomFileDialog(tk.Toplevel):
self.minsize(min_width, min_height)
self.title(title)
self.image = IconManager()
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 = parent
self.parent: tk.Widget = parent
self.transient(parent)
self.grab_set()
self.selected_file = None
self.current_dir = os.path.abspath(
self.selected_file: Optional[str] = None
self.current_dir: str = os.path.abspath(
initial_dir) if initial_dir else os.path.expanduser("~")
self.filetypes = filetypes if filetypes else [(LocaleStrings.CFD["all_files"], "*.* ")]
self.current_filter_pattern = self.filetypes[0][1]
self.history = []
self.history_pos = -1
self.view_mode = tk.StringVar(
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(value=False)
self.resize_job = None
self.last_width = 0
self.search_results = []
self.search_mode = False
self.original_path_text = ""
self.items_to_load_per_batch = 250
self.item_path_map = {}
self.responsive_buttons_hidden = None
self.search_job = None
self.search_thread = None
self.search_process = None
self.show_hidden_files: tk.BooleanVar = tk.BooleanVar(value=False)
self.resize_job: Optional[str] = None
self.last_width: int = 0
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()
self.style_manager = StyleManager(self)
self.icon_manager: IconManager = IconManager()
self.style_manager: StyleManager = StyleManager(self)
self.file_op_manager = FileOperationsManager(self)
self.search_manager = SearchManager(self)
self.navigation_manager = NavigationManager(self)
self.view_manager = ViewManager(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(self, self.settings)
self.widget_manager: WidgetManager = WidgetManager(self, self.settings)
self.widget_manager.filename_entry.bind("<Return>", self.search_manager.execute_search)
@@ -78,7 +96,8 @@ class CustomFileDialog(tk.Toplevel):
self.view_manager._update_view_mode_buttons()
def initial_load():
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())
@@ -94,18 +113,29 @@ class CustomFileDialog(tk.Toplevel):
if self.dialog_mode == "save":
self.bind("<Delete>", self.file_op_manager.delete_selected_item)
def load_settings(self):
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):
def get_min_size_from_preset(self, preset: str) -> Tuple[int, int]:
"""
Calculates the minimum window size based on a preset string.
Args:
preset: The size preset string (e.g., "1050x850").
Returns:
A tuple containing the minimum width and height.
"""
w, h = map(int, preset.split('x'))
return max(650, w - 400), max(450, h - 400)
def reload_config_and_rebuild_ui(self):
def reload_config_and_rebuild_ui(self) -> None:
"""Reloads the configuration and rebuilds the entire UI."""
self.load_settings()
self.geometry(self.settings["window_size_preset"])
@@ -139,10 +169,12 @@ class CustomFileDialog(tk.Toplevel):
else:
self.navigation_manager.navigate_to(self.current_dir)
def open_settings_dialog(self):
def open_settings_dialog(self) -> None:
"""Opens the settings dialog."""
SettingsDialog(self, dialog_mode=self.dialog_mode)
def update_animation_settings(self):
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
@@ -168,7 +200,17 @@ class CustomFileDialog(tk.Toplevel):
if is_running:
self.widget_manager.search_animation.start()
def get_file_icon(self, filename, size='large'):
def get_file_icon(self, filename: str, size: str = 'large') -> tk.PhotoImage:
"""
Gets the appropriate icon for a given filename.
Args:
filename: The name of the file.
size: The desired icon size ('large' or 'small').
Returns:
A PhotoImage object for the corresponding file type.
"""
ext = os.path.splitext(filename)[1].lower()
if ext == '.py':
@@ -188,7 +230,13 @@ class CustomFileDialog(tk.Toplevel):
return self.icon_manager.get_icon(f'iso_{size}')
return self.icon_manager.get_icon(f'file_{size}')
def on_window_resize(self, event):
def on_window_resize(self, event: tk.Event) -> None:
"""
Handles the window resize event.
Args:
event: The event object.
"""
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()
@@ -196,7 +244,8 @@ class CustomFileDialog(tk.Toplevel):
if self.resize_job:
self.after_cancel(self.resize_job)
def repopulate_icons():
def repopulate_icons() -> None:
"""Repopulates the file list icons."""
self.update_idletasks()
self.view_manager.populate_files()
@@ -205,7 +254,13 @@ class CustomFileDialog(tk.Toplevel):
self._handle_responsive_buttons(event.width)
def _handle_responsive_buttons(self, window_width):
def _handle_responsive_buttons(self, window_width: int) -> None:
"""
Shows or hides buttons based on the window width.
Args:
window_width: The current width of the window.
"""
threshold = 850
container = self.widget_manager.responsive_buttons_container
more_button = self.widget_manager.more_button
@@ -221,7 +276,8 @@ class CustomFileDialog(tk.Toplevel):
container.pack(side="left")
self.responsive_buttons_hidden = should_be_hidden
def show_more_menu(self):
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)
@@ -250,7 +306,13 @@ class CustomFileDialog(tk.Toplevel):
y = more_button.winfo_rooty() + more_button.winfo_height()
more_menu.tk_popup(x, y)
def on_sidebar_resize(self, event):
def on_sidebar_resize(self, event: tk.Event) -> None:
"""
Handles the sidebar resize event, adjusting button text visibility.
Args:
event: The event object.
"""
current_width = event.width
threshold_width = 100
@@ -265,11 +327,23 @@ class CustomFileDialog(tk.Toplevel):
for btn, original_text in self.widget_manager.device_buttons:
btn.config(text=original_text, compound="left")
def _on_devices_enter(self, event):
def _on_devices_enter(self, event: tk.Event) -> None:
"""
Shows the scrollbar when the mouse enters the devices area.
Args:
event: The event object.
"""
self.widget_manager.devices_scrollbar.grid(
row=1, column=1, sticky="ns")
def _on_devices_leave(self, event):
def _on_devices_leave(self, event: tk.Event) -> None:
"""
Hides the scrollbar when the mouse leaves the devices area.
Args:
event: The event object.
"""
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()
@@ -281,7 +355,8 @@ class CustomFileDialog(tk.Toplevel):
widget_y - buffer <= y <= widget_y + widget_height + buffer):
self.widget_manager.devices_scrollbar.grid_remove()
def toggle_recursive_search(self):
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():
@@ -291,7 +366,13 @@ class CustomFileDialog(tk.Toplevel):
self.widget_manager.recursive_button.configure(
style="Header.TButton.Borderless.Round")
def update_status_bar(self, selected_path=None):
def update_status_bar(self, selected_path: Optional[str] = None) -> None:
"""
Updates the status bar with disk usage and selected item information.
Args:
selected_path: The path of the currently selected item.
"""
try:
total, used, free = shutil.disk_usage(self.current_dir)
free_str = self._format_size(free)
@@ -320,6 +401,206 @@ class CustomFileDialog(tk.Toplevel):
text=f"{LocaleStrings.CFD["free_space"]}: {LocaleStrings.CFD["unknown"]}")
self.widget_manager.storage_bar['value'] = 0
def on_open(self) -> None:
"""Handles the 'Open' action, closing the dialog if a file is selected."""
if self.selected_file and os.path.isfile(self.selected_file):
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.selected_file = 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.selected_file = None
self.destroy()
def get_selected_file(self) -> Optional[str]:
"""
Returns the path of the selected file.
Returns:
The selected file path, or None if no file was selected.
"""
return self.selected_file
def update_action_buttons_state(self) -> None:
"""Updates the state of action buttons (e.g., 'New Folder') based on directory permissions."""
is_writable = os.access(self.current_dir, os.W_OK)
state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED
self.widget_manager.new_folder_button.config(state=state)
self.widget_manager.new_file_button.config(state=state)
def _matches_filetype(self, filename: str) -> bool:
"""
Checks if a filename matches the current filetype filter.
Args:
filename: The name of the file to check.
Returns:
True if the file matches the filter, False otherwise.
"""
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).
Args:
size_bytes: The size in bytes.
Returns:
A formatted string representing the size.
"""
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.
Args:
text: The text to shorten.
max_len: The maximum allowed length.
Returns:
The shortened text.
"""
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.
Returns:
A list of tuples, where each tuple contains the display name,
mount point, and a boolean indicating if it's removable.
"""
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
def _show_tooltip(self, event: tk.Event) -> None:
"""
Displays a tooltip for the search animation icon.
Args:
event: The event object.
"""
if hasattr(self, 'tooltip_window') and self.tooltip_window.winfo_exists():
return
tooltip_text = LocaleStrings.UI["start_search"] if not self.widget_manager.search_animation.running else LocaleStrings.UI["cancel_search"]
x = self.widget_manager.search_animation.winfo_rootx() + 25
y = self.widget_manager.search_animation.winfo_rooty() + 25
self.tooltip_window = tk.Toplevel(self)
self.tooltip_window.wm_overrideredirect(True)
self.tooltip_window.wm_geometry(f"+{x}+{y}")
label = tk.Label(self.tooltip_window, text=tooltip_text, relief="solid", borderwidth=1)
label.pack()
def _hide_tooltip(self, event: tk.Event) -> None:
"""
Hides the tooltip.
Args:
event: The event object.
"""
if hasattr(self, 'tooltip_window') and self.tooltip_window.winfo_exists():
self.tooltip_window.destroy()
"'{os.path.basename(selected_path)}' ({content_count} {LocaleStrings.CFD["entries"]})"
else:
status_text = f"'{os.path.basename(selected_path)}'"
else:
size = os.path.getsize(selected_path)
size_str = self._format_size(size)
status_text = f"'{os.path.basename(selected_path)}' {LocaleStrings.VIEW["size"]}: {size_str}"
self.widget_manager.search_status_label.config(text=status_text)
except FileNotFoundError:
self.widget_manager.search_status_label.config(
text=LocaleStrings.CFD["directory_not_found"])
self.widget_manager.storage_label.config(
text=f"{LocaleStrings.CFD["free_space"]}: {LocaleStrings.CFD["unknown"]}")
self.widget_manager.storage_bar['value'] = 0
def on_open(self):
if self.selected_file and os.path.isfile(self.selected_file):
self.destroy()