727 lines
31 KiB
Python
727 lines
31 KiB
Python
import os
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from datetime import datetime
|
|
from typing import Optional, List, Tuple, Callable, Any, Dict
|
|
|
|
# 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 shared_libs.message import MessageDialog
|
|
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
|
|
|
|
|
class ViewManager:
|
|
"""Manages the display of files and folders in list and icon views."""
|
|
|
|
def __init__(self, dialog: 'CustomFileDialog'):
|
|
"""
|
|
Initializes the ViewManager.
|
|
|
|
Args:
|
|
dialog: The main CustomFileDialog instance.
|
|
"""
|
|
self.dialog = dialog
|
|
|
|
def _get_file_info_list(self) -> Tuple[List[Dict], Optional[str], Optional[str]]:
|
|
"""
|
|
Gets a sorted list of file information dictionaries from the current source.
|
|
"""
|
|
if self.dialog.current_fs_type == "sftp":
|
|
items, error = self.dialog.sftp_manager.list_directory(self.dialog.current_dir)
|
|
if error:
|
|
return [], error, None
|
|
|
|
file_info_list = []
|
|
import stat
|
|
for item in items:
|
|
if item.filename in ['.', '..']:
|
|
continue
|
|
is_dir = stat.S_ISDIR(item.st_mode)
|
|
# Manually construct SFTP path to ensure forward slashes
|
|
path = f"{self.dialog.current_dir}/{item.filename}".replace("//", "/")
|
|
file_info_list.append({
|
|
'name': item.filename,
|
|
'path': path,
|
|
'is_dir': is_dir,
|
|
'size': item.st_size,
|
|
'modified': item.st_mtime
|
|
})
|
|
return file_info_list, None, None
|
|
|
|
else:
|
|
try:
|
|
items = list(os.scandir(self.dialog.current_dir))
|
|
num_items = len(items)
|
|
warning_message = None
|
|
if num_items > CfdConfigManager.MAX_ITEMS_TO_DISPLAY:
|
|
warning_message = f"{LocaleStrings.CFD['showing']} {CfdConfigManager.MAX_ITEMS_TO_DISPLAY} {LocaleStrings.CFD['of']} {num_items} {LocaleStrings.CFD['entries']}."
|
|
items = items[:CfdConfigManager.MAX_ITEMS_TO_DISPLAY]
|
|
|
|
items.sort(key=lambda e: (not e.is_dir(), e.name.lower()))
|
|
|
|
file_info_list = []
|
|
for item in items:
|
|
try:
|
|
stat_result = item.stat()
|
|
file_info_list.append({
|
|
'name': item.name,
|
|
'path': item.path,
|
|
'is_dir': item.is_dir(),
|
|
'size': stat_result.st_size,
|
|
'modified': stat_result.st_mtime
|
|
})
|
|
except (FileNotFoundError, PermissionError):
|
|
continue
|
|
|
|
return file_info_list, None, warning_message
|
|
except PermissionError:
|
|
return ([], LocaleStrings.CFD["access_denied"], None)
|
|
except FileNotFoundError:
|
|
return ([], LocaleStrings.CFD["directory_not_found"], None)
|
|
|
|
def populate_files(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
|
"""
|
|
Populates the main file display area.
|
|
"""
|
|
self._unbind_mouse_wheel_events()
|
|
|
|
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
|
|
widget.destroy()
|
|
self.dialog.widget_manager.path_entry.delete(0, tk.END)
|
|
self.dialog.widget_manager.path_entry.insert(
|
|
0, self.dialog.current_dir)
|
|
self.dialog.result = None
|
|
self.dialog.selected_item_frames.clear()
|
|
self.dialog.update_selection_info()
|
|
if self.dialog.view_mode.get() == "list":
|
|
self.populate_list_view(item_to_rename, item_to_select)
|
|
else:
|
|
self.populate_icon_view(item_to_rename, item_to_select)
|
|
|
|
def _get_folder_content_count(self, folder_path: str) -> Optional[int]:
|
|
"""
|
|
Counts the number of items in a given folder, supporting both local and SFTP.
|
|
"""
|
|
try:
|
|
if self.dialog.current_fs_type == "sftp":
|
|
if not self.dialog.sftp_manager.path_is_dir(folder_path):
|
|
return None
|
|
items, error = self.dialog.sftp_manager.list_directory(folder_path)
|
|
if error:
|
|
return None
|
|
else:
|
|
if not os.path.isdir(folder_path) or not os.access(folder_path, os.R_OK):
|
|
return None
|
|
items = os.listdir(folder_path)
|
|
|
|
if not self.dialog.show_hidden_files.get():
|
|
# For SFTP, items are attrs, for local they are strings
|
|
if self.dialog.current_fs_type == "sftp":
|
|
items = [item for item in items if not item.filename.startswith('.')]
|
|
else:
|
|
items = [item for item in items if not item.startswith('.')]
|
|
|
|
return len(items)
|
|
except (PermissionError, FileNotFoundError):
|
|
return None
|
|
|
|
def _get_item_path_from_widget(self, widget: tk.Widget) -> Optional[str]:
|
|
"""
|
|
Traverses up the widget hierarchy to find the item_path attribute.
|
|
"""
|
|
while widget and not hasattr(widget, 'item_path'):
|
|
widget = widget.master
|
|
return getattr(widget, 'item_path', None)
|
|
|
|
def _is_dir(self, path: str) -> bool:
|
|
"""Checks if a given path is a directory, supporting both local and SFTP."""
|
|
if self.dialog.current_fs_type == 'sftp':
|
|
for item in self.dialog.all_items:
|
|
if item['path'] == path:
|
|
return item['is_dir']
|
|
return False
|
|
else:
|
|
return os.path.isdir(path)
|
|
|
|
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:
|
|
item_frame = event.widget
|
|
while not hasattr(item_frame, 'item_path'):
|
|
item_frame = item_frame.master
|
|
self.on_item_select(item_path, item_frame, 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._handle_item_double_click(item_path)
|
|
|
|
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: 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:
|
|
item_frame = event.widget
|
|
while not hasattr(item_frame, 'item_path'):
|
|
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: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
|
"""
|
|
Populates the file display with items in an icon grid layout.
|
|
"""
|
|
self.dialog.all_items, error, warning = self._get_file_info_list()
|
|
self.dialog.currently_loaded_count = 0
|
|
|
|
self.dialog.icon_canvas = tk.Canvas(self.dialog.widget_manager.file_list_frame,
|
|
highlightthickness=0, bg=self.dialog.style_manager.icon_bg_color)
|
|
v_scrollbar = ttk.Scrollbar(
|
|
self.dialog.widget_manager.file_list_frame, orient="vertical", command=self.dialog.icon_canvas.yview)
|
|
self.dialog.icon_canvas.pack(side="left", fill="both", expand=True)
|
|
self.dialog.icon_canvas.focus_set()
|
|
v_scrollbar.pack(side="right", fill="y")
|
|
container_frame = ttk.Frame(
|
|
self.dialog.icon_canvas, style="Content.TFrame")
|
|
self.dialog.icon_canvas.create_window(
|
|
(0, 0), window=container_frame, anchor="nw")
|
|
container_frame.bind("<Configure>", lambda e: self.dialog.icon_canvas.configure(
|
|
scrollregion=self.dialog.icon_canvas.bbox("all")))
|
|
|
|
def _on_mouse_wheel(event: tk.Event) -> None:
|
|
if event.num == 4:
|
|
delta = -1
|
|
elif event.num == 5:
|
|
delta = 1
|
|
else:
|
|
delta = -1 * int(event.delta / 120)
|
|
self.dialog.icon_canvas.yview_scroll(delta, "units")
|
|
if self.dialog.currently_loaded_count < len(self.dialog.all_items) and self.dialog.icon_canvas.yview()[1] > 0.9:
|
|
self._load_more_items_icon_view(
|
|
container_frame, _on_mouse_wheel)
|
|
|
|
for widget in [self.dialog.icon_canvas, container_frame]:
|
|
widget.bind("<MouseWheel>", _on_mouse_wheel)
|
|
widget.bind("<Button-4>", _on_mouse_wheel)
|
|
widget.bind("<Button-5>", _on_mouse_wheel)
|
|
|
|
if warning:
|
|
self.dialog.widget_manager.search_status_label.config(text=warning)
|
|
if error:
|
|
ttk.Label(container_frame, text=error).pack(pady=20)
|
|
return
|
|
|
|
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() -> None:
|
|
self.dialog.update_idletasks()
|
|
if not widget_to_focus.winfo_exists():
|
|
return
|
|
y = widget_to_focus.winfo_y()
|
|
canvas_height = self.dialog.icon_canvas.winfo_height()
|
|
scroll_region = self.dialog.icon_canvas.bbox("all")
|
|
if not scroll_region:
|
|
return
|
|
scroll_height = scroll_region[3]
|
|
if scroll_height > canvas_height:
|
|
fraction = y / scroll_height
|
|
self.dialog.icon_canvas.yview_moveto(fraction)
|
|
|
|
self.dialog.after(100, scroll_to_widget)
|
|
|
|
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.
|
|
"""
|
|
start_index = self.dialog.currently_loaded_count
|
|
end_index = min(len(self.dialog.all_items), start_index +
|
|
self.dialog.items_to_load_per_batch)
|
|
|
|
if start_index >= end_index:
|
|
return None
|
|
|
|
item_width, item_height = 125, 100
|
|
frame_width = self.dialog.widget_manager.file_list_frame.winfo_width()
|
|
col_count = max(1, frame_width // item_width - 1)
|
|
|
|
row = start_index // col_count if col_count > 0 else 0
|
|
col = start_index % col_count if col_count > 0 else 0
|
|
|
|
widget_to_focus = None
|
|
|
|
for i in range(start_index, end_index):
|
|
file_info = self.dialog.all_items[i]
|
|
name = file_info['name']
|
|
path = file_info['path']
|
|
is_dir = file_info['is_dir']
|
|
|
|
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
|
continue
|
|
if not is_dir and not self.dialog._matches_filetype(name):
|
|
continue
|
|
|
|
item_frame = ttk.Frame(
|
|
container, width=item_width, height=item_height, style="Item.TFrame")
|
|
item_frame.grid(row=row, column=col, padx=5, ipadx=25, pady=5)
|
|
item_frame.grid_propagate(False)
|
|
item_frame.item_path = path
|
|
|
|
if name == item_to_rename:
|
|
self.dialog.file_op_manager.start_rename(item_frame, path)
|
|
widget_to_focus = item_frame
|
|
else:
|
|
icon = self.dialog.icon_manager.get_icon(
|
|
'folder_large') if is_dir else self.dialog.get_file_icon(name, 'large')
|
|
icon_label = ttk.Label(
|
|
item_frame, image=icon, style="Icon.TLabel")
|
|
icon_label.pack(pady=(10, 5))
|
|
name_label = ttk.Label(item_frame, text=self.dialog.shorten_text(
|
|
name, 14), anchor="center", style="Item.TLabel")
|
|
name_label.pack(fill="x", expand=True)
|
|
|
|
Tooltip(item_frame, name)
|
|
|
|
for widget in [item_frame, icon_label, name_label]:
|
|
widget.bind("<Double-Button-1>", lambda e,
|
|
p=path: self._handle_item_double_click(p))
|
|
widget.bind("<Button-1>", lambda e, p=path,
|
|
f=item_frame: self.on_item_select(p, f, e))
|
|
widget.bind("<ButtonRelease-3>", lambda e,
|
|
p=path: self.dialog.file_op_manager._show_context_menu(e, p))
|
|
widget.bind("<F2>", lambda e, p=path,
|
|
f=item_frame: self.dialog.file_op_manager.on_rename_request(e, p, f))
|
|
widget.bind("<MouseWheel>", scroll_handler)
|
|
widget.bind("<Button-4>", scroll_handler)
|
|
widget.bind("<Button-5>", scroll_handler)
|
|
|
|
if name == item_to_select:
|
|
self.on_item_select(path, item_frame)
|
|
widget_to_focus = item_frame
|
|
|
|
if col_count > 0:
|
|
col = (col + 1) % col_count
|
|
if col == 0:
|
|
row += 1
|
|
else:
|
|
row += 1
|
|
|
|
self.dialog.currently_loaded_count = end_index
|
|
return widget_to_focus
|
|
|
|
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.
|
|
"""
|
|
self.dialog.all_items, error, warning = self._get_file_info_list()
|
|
self.dialog.currently_loaded_count = 0
|
|
|
|
tree_frame = ttk.Frame(self.dialog.widget_manager.file_list_frame)
|
|
tree_frame.pack(fill='both', expand=True)
|
|
tree_frame.grid_rowconfigure(0, weight=1)
|
|
tree_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
columns = ("size", "type", "modified")
|
|
self.dialog.tree = ttk.Treeview(
|
|
tree_frame, columns=columns, show="tree headings")
|
|
|
|
if self.dialog.dialog_mode == 'multi':
|
|
self.dialog.tree.config(selectmode="extended")
|
|
|
|
self.dialog.tree.heading(
|
|
"#0", text=LocaleStrings.VIEW["name"], anchor="w")
|
|
self.dialog.tree.column("#0", anchor="w", width=250, stretch=True)
|
|
self.dialog.tree.heading(
|
|
"size", text=LocaleStrings.VIEW["size"], anchor="e")
|
|
self.dialog.tree.column("size", anchor="e", width=120, stretch=False)
|
|
self.dialog.tree.heading(
|
|
"type", text=LocaleStrings.VIEW["type"], anchor="w")
|
|
self.dialog.tree.column("type", anchor="w", width=120, stretch=False)
|
|
self.dialog.tree.heading(
|
|
"modified", text=LocaleStrings.VIEW["date_modified"], anchor="w")
|
|
self.dialog.tree.column("modified", anchor="w",
|
|
width=160, stretch=False)
|
|
|
|
v_scrollbar = ttk.Scrollbar(
|
|
tree_frame, orient="vertical", command=self.dialog.tree.yview)
|
|
h_scrollbar = ttk.Scrollbar(
|
|
tree_frame, orient="horizontal", command=self.dialog.tree.xview)
|
|
self.dialog.tree.configure(yscrollcommand=v_scrollbar.set,
|
|
xscrollcommand=h_scrollbar.set)
|
|
|
|
self.dialog.tree.grid(row=0, column=0, sticky='nsew')
|
|
self.dialog.tree.focus_set()
|
|
v_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
h_scrollbar.grid(row=1, column=0, sticky='ew')
|
|
|
|
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)
|
|
self.dialog.tree.configure(yscrollcommand=_on_scroll)
|
|
|
|
self.dialog.tree.bind("<Double-1>", self.on_list_double_click)
|
|
self.dialog.tree.bind("<<TreeviewSelect>>", self.on_list_select)
|
|
self.dialog.tree.bind(
|
|
"<F2>", self.dialog.file_op_manager.on_rename_request)
|
|
self.dialog.tree.bind("<ButtonRelease-3>", self.on_list_context_menu)
|
|
|
|
if warning:
|
|
self.dialog.widget_manager.search_status_label.config(text=warning)
|
|
if error:
|
|
self.dialog.tree.insert("", "end", text=error, values=())
|
|
return
|
|
|
|
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: Optional[str] = None, item_to_select: Optional[str] = None) -> bool:
|
|
"""
|
|
Loads a batch of items into the list view.
|
|
"""
|
|
start_index = self.dialog.currently_loaded_count
|
|
end_index = min(len(self.dialog.all_items), start_index +
|
|
self.dialog.items_to_load_per_batch)
|
|
|
|
if start_index >= end_index:
|
|
return False
|
|
|
|
item_found = False
|
|
for i in range(start_index, end_index):
|
|
file_info = self.dialog.all_items[i]
|
|
name = file_info['name']
|
|
path = file_info['path']
|
|
is_dir = file_info['is_dir']
|
|
|
|
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
|
continue
|
|
if not is_dir and not self.dialog._matches_filetype(name):
|
|
continue
|
|
try:
|
|
modified_time = datetime.fromtimestamp(
|
|
file_info['modified']).strftime('%d.%m.%Y %H:%M')
|
|
if is_dir:
|
|
icon, file_type, size = self.dialog.icon_manager.get_icon(
|
|
'folder_small'), LocaleStrings.FILE["folder"], ""
|
|
else:
|
|
icon, file_type, size = self.dialog.get_file_icon(
|
|
name, 'small'), LocaleStrings.FILE["file"], self.dialog._format_size(file_info['size'])
|
|
item_id = self.dialog.tree.insert("", "end", text=f" {name}", image=icon, values=(
|
|
size, file_type, modified_time))
|
|
self.dialog.item_path_map[item_id] = path # Store path for later retrieval
|
|
|
|
if name == item_to_rename:
|
|
self.dialog.tree.selection_set(item_id)
|
|
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: str, item_frame: ttk.Frame, event: Optional[tk.Event] = None) -> None:
|
|
"""
|
|
Handles the selection of an item in the icon view.
|
|
"""
|
|
if self.dialog.dialog_mode == 'dir' and not self._is_dir(path):
|
|
return
|
|
|
|
if self.dialog.dialog_mode == 'multi':
|
|
ctrl_pressed = (event.state & 0x4) != 0 if event else False
|
|
|
|
if ctrl_pressed:
|
|
if item_frame in self.dialog.selected_item_frames:
|
|
item_frame.state(['!selected'])
|
|
for child in item_frame.winfo_children():
|
|
if isinstance(child, ttk.Label):
|
|
child.state(['!selected'])
|
|
self.dialog.selected_item_frames.remove(item_frame)
|
|
else:
|
|
item_frame.state(['selected'])
|
|
for child in item_frame.winfo_children():
|
|
if isinstance(child, ttk.Label):
|
|
child.state(['selected'])
|
|
self.dialog.selected_item_frames.append(item_frame)
|
|
else:
|
|
for f in self.dialog.selected_item_frames:
|
|
f.state(['!selected'])
|
|
for child in f.winfo_children():
|
|
if isinstance(child, ttk.Label):
|
|
child.state(['!selected'])
|
|
self.dialog.selected_item_frames.clear()
|
|
|
|
item_frame.state(['selected'])
|
|
for child in item_frame.winfo_children():
|
|
if isinstance(child, ttk.Label):
|
|
child.state(['selected'])
|
|
self.dialog.selected_item_frames.append(item_frame)
|
|
|
|
selected_paths = [frame.item_path for frame in self.dialog.selected_item_frames]
|
|
self.dialog.result = selected_paths
|
|
self.dialog.update_selection_info()
|
|
|
|
else: # Single selection mode
|
|
if hasattr(self.dialog, 'selected_item_frame') and self.dialog.selected_item_frame.winfo_exists():
|
|
self.dialog.selected_item_frame.state(['!selected'])
|
|
for child in self.dialog.selected_item_frame.winfo_children():
|
|
if isinstance(child, ttk.Label):
|
|
child.state(['!selected'])
|
|
item_frame.state(['selected'])
|
|
for child in item_frame.winfo_children():
|
|
if isinstance(child, ttk.Label):
|
|
child.state(['selected'])
|
|
self.dialog.selected_item_frame = item_frame
|
|
self.dialog.update_selection_info(path)
|
|
|
|
self.dialog.search_manager.show_search_ready()
|
|
|
|
def on_list_select(self, event: tk.Event) -> None:
|
|
"""
|
|
Handles the selection of an item in the list view.
|
|
"""
|
|
selections = self.dialog.tree.selection()
|
|
if not selections:
|
|
self.dialog.result = [] if self.dialog.dialog_mode == 'multi' else None
|
|
self.dialog.update_selection_info()
|
|
return
|
|
|
|
if self.dialog.dialog_mode == 'multi':
|
|
selected_paths = []
|
|
for item_id in selections:
|
|
path = self.dialog.item_path_map.get(item_id)
|
|
if path:
|
|
selected_paths.append(path)
|
|
self.dialog.result = selected_paths
|
|
self.dialog.update_selection_info()
|
|
else:
|
|
item_id = selections[0]
|
|
path = self.dialog.item_path_map.get(item_id)
|
|
if not path:
|
|
return
|
|
|
|
if self.dialog.dialog_mode == 'dir' and not self._is_dir(path):
|
|
self.dialog.result = None
|
|
self.dialog.tree.selection_remove(item_id)
|
|
self.dialog.update_selection_info()
|
|
return
|
|
|
|
self.dialog.update_selection_info(path)
|
|
|
|
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:
|
|
return "break"
|
|
self.dialog.tree.selection_set(iid)
|
|
path = self.dialog.item_path_map.get(iid)
|
|
if path:
|
|
self.dialog.file_op_manager._show_context_menu(event, path)
|
|
return "break"
|
|
|
|
def _handle_item_double_click(self, path: str) -> None:
|
|
"""
|
|
Handles the logic for a double-click on any item, regardless of view.
|
|
"""
|
|
if self._is_dir(path):
|
|
if self.dialog.dialog_mode == 'dir':
|
|
has_subdirs = False
|
|
try:
|
|
if self.dialog.current_fs_type == "sftp":
|
|
import stat
|
|
items, _ = self.dialog.sftp_manager.list_directory(path)
|
|
for item in items:
|
|
if item.filename not in ['.', '..'] and stat.S_ISDIR(item.st_mode):
|
|
has_subdirs = True
|
|
break
|
|
else:
|
|
for item in os.listdir(path):
|
|
if os.path.isdir(os.path.join(path, item)) and not item.startswith('.'):
|
|
has_subdirs = True
|
|
break
|
|
except OSError:
|
|
self.dialog.navigation_manager.navigate_to(path)
|
|
return
|
|
|
|
if has_subdirs:
|
|
self.dialog.navigation_manager.navigate_to(path)
|
|
else:
|
|
dialog = MessageDialog(
|
|
master=self.dialog,
|
|
message_type="ask",
|
|
title=LocaleStrings.CFD["select_or_enter_title"],
|
|
text=LocaleStrings.CFD["select_or_enter_prompt"].format(folder_name=os.path.basename(path)),
|
|
buttons=[
|
|
LocaleStrings.CFD["select_button"],
|
|
LocaleStrings.CFD["enter_button"],
|
|
LocaleStrings.CFD["cancel_button"],
|
|
]
|
|
)
|
|
choice = dialog.show()
|
|
|
|
if choice is True:
|
|
self.dialog.result = path
|
|
self.dialog.on_open()
|
|
elif choice is False:
|
|
self.dialog.navigation_manager.navigate_to(path)
|
|
else:
|
|
self.dialog.navigation_manager.navigate_to(path)
|
|
|
|
elif self.dialog.dialog_mode in ["open", "multi"]:
|
|
self.dialog.result = path
|
|
self.dialog.on_open()
|
|
elif self.dialog.dialog_mode == "save":
|
|
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.dialog.widget_manager.filename_entry.insert(
|
|
0, os.path.basename(path))
|
|
self.dialog.on_save()
|
|
|
|
def on_list_double_click(self, event: tk.Event) -> None:
|
|
"""
|
|
Handles a double-click on a list view item.
|
|
"""
|
|
selection = self.dialog.tree.selection()
|
|
if not selection:
|
|
return
|
|
item_id = selection[0]
|
|
path = self.dialog.item_path_map.get(item_id)
|
|
if path:
|
|
self._handle_item_double_click(path)
|
|
|
|
def _select_file_in_view(self, filename: str) -> None:
|
|
"""
|
|
Programmatically selects a file in the current view.
|
|
"""
|
|
is_sftp = self.dialog.current_fs_type == "sftp"
|
|
|
|
if self.dialog.view_mode.get() == "list":
|
|
for item_id, path in self.dialog.item_path_map.items():
|
|
basename = path.split('/')[-1] if is_sftp else os.path.basename(path)
|
|
if basename == filename:
|
|
self.dialog.tree.selection_set(item_id)
|
|
self.dialog.tree.focus(item_id)
|
|
self.dialog.tree.see(item_id)
|
|
break
|
|
elif self.dialog.view_mode.get() == "icons":
|
|
if not hasattr(self.dialog, 'icon_canvas') or not self.dialog.icon_canvas.winfo_exists():
|
|
return
|
|
|
|
container_frame = self.dialog.icon_canvas.winfo_children()[0]
|
|
|
|
if is_sftp:
|
|
# Ensure forward slashes for SFTP paths
|
|
target_path = f"{self.dialog.current_dir}/{filename}".replace("//", "/")
|
|
else:
|
|
target_path = os.path.join(self.dialog.current_dir, filename)
|
|
|
|
for widget in container_frame.winfo_children():
|
|
if hasattr(widget, 'item_path') and widget.item_path == target_path:
|
|
self.on_item_select(widget.item_path, widget)
|
|
|
|
def scroll_to_widget() -> None:
|
|
self.dialog.update_idletasks()
|
|
if not widget.winfo_exists():
|
|
return
|
|
y = widget.winfo_y()
|
|
canvas_height = self.dialog.icon_canvas.winfo_height()
|
|
scroll_region = self.dialog.icon_canvas.bbox("all")
|
|
if not scroll_region:
|
|
return
|
|
|
|
scroll_height = scroll_region[3]
|
|
if scroll_height > canvas_height:
|
|
fraction = y / scroll_height
|
|
self.dialog.icon_canvas.yview_moveto(fraction)
|
|
|
|
self.dialog.after(100, scroll_to_widget)
|
|
break
|
|
|
|
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(
|
|
style="Header.TButton.Active.Round")
|
|
self.dialog.widget_manager.list_view_button.configure(
|
|
style="Header.TButton.Borderless.Round")
|
|
else:
|
|
self.dialog.widget_manager.list_view_button.configure(
|
|
style="Header.TButton.Active.Round")
|
|
self.dialog.widget_manager.icon_view_button.configure(
|
|
style="Header.TButton.Borderless.Round")
|
|
|
|
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) -> 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) -> 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():
|
|
self.dialog.widget_manager.hidden_files_button.config(
|
|
image=self.dialog.icon_manager.get_icon('unhide'))
|
|
Tooltip(self.dialog.widget_manager.hidden_files_button,
|
|
LocaleStrings.UI["hide_hidden_files"])
|
|
else:
|
|
self.dialog.widget_manager.hidden_files_button.config(
|
|
image=self.dialog.icon_manager.get_icon('hide'))
|
|
Tooltip(self.dialog.widget_manager.hidden_files_button,
|
|
LocaleStrings.UI["show_hidden_files"])
|
|
self.populate_files()
|
|
|
|
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:
|
|
if desc == selected_desc:
|
|
self.dialog.current_filter_pattern = pattern
|
|
break
|
|
self.populate_files()
|
|
|
|
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>")
|
|
self.dialog.unbind_all("<Button-5>") |