Files
shared_libs/custom_file_dialog.py

450 lines
20 KiB
Python

import os
import shutil
import tkinter as tk
from tkinter import ttk
from datetime import datetime
import subprocess
import json
import threading
from shared_libs.message import MessageDialog
from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools
from cfd_app_config import AppConfig, CfdConfigManager, LocaleStrings, _
from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir
from animated_icon import AnimatedIcon, PIL_AVAILABLE
from cfd_settings_dialog import SettingsDialog
from cfd_file_operations import FileOperationsManager
from cfd_search_manager import SearchManager
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"]):
super().__init__(parent)
self.my_tool_tip = None
self.dialog_mode = dialog_mode
self.load_settings()
self.geometry(self.settings["window_size_preset"])
min_width, min_height = self.get_min_size_from_preset(
self.settings["window_size_preset"])
self.minsize(min_width, min_height)
self.title(title)
self.image = IconManager()
width, height = map(
int, self.settings["window_size_preset"].split('x'))
LxTools.center_window_cross_platform(self, width, height)
self.parent = parent
self.transient(parent)
self.grab_set()
self.selected_file = None
self.current_dir = 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(
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.icon_manager = IconManager()
self.style_manager = StyleManager(self)
self.file_op_manager = FileOperationsManager(self)
self.search_manager = SearchManager(self)
self.navigation_manager = NavigationManager(self)
self.view_manager = ViewManager(self)
self.widget_manager = WidgetManager(self, self.settings)
self.widget_manager.filename_entry.bind("<Return>", self.search_manager.execute_search)
self.update_animation_settings()
self.view_manager._update_view_mode_buttons()
def initial_load():
self.update_idletasks()
self.last_width = self.widget_manager.file_list_frame.winfo_width()
self._handle_responsive_buttons(self.winfo_width())
self.navigation_manager.navigate_to(self.current_dir)
self.after(10, initial_load)
self.widget_manager.path_entry.bind(
"<Return>", self.navigation_manager.handle_path_entry_return)
self.bind("<Key>", self.search_manager.show_search_bar)
if self.dialog_mode == "save":
self.bind("<Delete>", self.file_op_manager.delete_selected_item)
def load_settings(self):
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):
w, h = map(int, preset.split('x'))
return max(650, w - 400), max(450, h - 400)
def reload_config_and_rebuild_ui(self):
self.load_settings()
self.geometry(self.settings["window_size_preset"])
min_width, min_height = self.get_min_size_from_preset(
self.settings["window_size_preset"])
self.minsize(min_width, min_height)
width, height = map(
int, self.settings["window_size_preset"].split('x'))
LxTools.center_window_cross_platform(self, width, height)
for widget in self.winfo_children():
widget.destroy()
self.style_manager = StyleManager(self)
self.file_op_manager = FileOperationsManager(self)
self.search_manager = SearchManager(self)
self.navigation_manager = NavigationManager(self)
self.view_manager = ViewManager(self)
self.widget_manager = WidgetManager(self, self.settings)
self.widget_manager.filename_entry.bind("<Return>", self.search_manager.execute_search)
self.view_manager._update_view_mode_buttons()
self.responsive_buttons_hidden = None
self.update_idletasks()
self._handle_responsive_buttons(self.winfo_width())
self.update_animation_settings()
if self.search_mode:
self.search_manager.show_search_results_treeview()
else:
self.navigation_manager.navigate_to(self.current_dir)
def open_settings_dialog(self):
SettingsDialog(self, dialog_mode=self.dialog_mode)
def update_animation_settings(self):
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
if is_running:
self.widget_manager.search_animation.stop()
self.widget_manager.search_animation.destroy()
self.widget_manager.search_animation = AnimatedIcon(
self.widget_manager.status_container,
width=23,
height=23,
use_pillow=use_pillow,
animation_type=anim_type,
color="#2a6fde",
highlight_color="#5195ff",
bg=self.style_manager.bottom_color
)
self.widget_manager.search_animation.grid(row=0, column=0, sticky='w', padx=(0, 5), pady=(4,0))
self.widget_manager.search_animation.bind("<Button-1>", lambda e: self.search_manager.activate_search())
self.widget_manager.search_animation.bind("<Enter>", self._show_tooltip)
self.widget_manager.search_animation.bind("<Leave>", self._hide_tooltip)
if is_running:
self.widget_manager.search_animation.start()
def get_file_icon(self, filename, size='large'):
ext = os.path.splitext(filename)[1].lower()
if ext == '.py':
return self.icon_manager.get_icon(f'python_{size}')
if ext == '.pdf':
return self.icon_manager.get_icon(f'pdf_{size}')
if ext in ['.tar', '.zip', '.rar', '.7z', '.gz']:
return self.icon_manager.get_icon(f'archive_{size}')
if ext in ['.mp3', '.wav', '.ogg', '.flac']:
return self.icon_manager.get_icon(f'audio_{size}')
if ext in ['.mp4', '.mkv', '.avi', '.mov']:
return self.icon_manager.get_icon(f'video_{size}') if size == 'large' else self.icon_manager.get_icon(
'video_small_file')
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg']:
return self.icon_manager.get_icon(f'picture_{size}')
if ext == '.iso':
return self.icon_manager.get_icon(f'iso_{size}')
return self.icon_manager.get_icon(f'file_{size}')
def on_window_resize(self, event):
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()
if abs(new_width - self.last_width) > 50:
if self.resize_job:
self.after_cancel(self.resize_job)
def repopulate_icons():
self.update_idletasks()
self.view_manager.populate_files()
self.resize_job = self.after(150, repopulate_icons)
self.last_width = new_width
self._handle_responsive_buttons(event.width)
def _handle_responsive_buttons(self, window_width):
threshold = 850
container = self.widget_manager.responsive_buttons_container
more_button = self.widget_manager.more_button
should_be_hidden = window_width < threshold
if should_be_hidden != self.responsive_buttons_hidden:
if should_be_hidden:
container.pack_forget()
more_button.pack(side="left", padx=5)
else:
more_button.pack_forget()
container.pack(side="left")
self.responsive_buttons_hidden = should_be_hidden
def show_more_menu(self):
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)
is_writable = os.access(self.current_dir, os.W_OK)
creation_state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED
more_menu.add_command(label=LocaleStrings.UI["new_folder"], command=self.file_op_manager.create_new_folder,
image=self.icon_manager.get_icon('new_folder_small'), compound='left', state=creation_state)
more_menu.add_command(label=LocaleStrings.UI["new_document"], command=self.file_op_manager.create_new_file,
image=self.icon_manager.get_icon('new_document_small'), compound='left', state=creation_state)
more_menu.add_separator()
more_menu.add_command(label=LocaleStrings.VIEW["icon_view"], command=self.view_manager.set_icon_view,
image=self.icon_manager.get_icon('icon_view'), compound='left')
more_menu.add_command(label=LocaleStrings.VIEW["list_view"], command=self.view_manager.set_list_view,
image=self.icon_manager.get_icon('list_view'), compound='left')
more_menu.add_separator()
hidden_files_label = LocaleStrings.UI["hide_hidden_files"] if self.show_hidden_files.get() else LocaleStrings.UI["show_hidden_files"]
hidden_files_icon = self.icon_manager.get_icon(
'unhide') if self.show_hidden_files.get() else self.icon_manager.get_icon('hide')
more_menu.add_command(label=hidden_files_label, command=self.view_manager.toggle_hidden_files,
image=hidden_files_icon, compound='left')
more_button = self.widget_manager.more_button
x = more_button.winfo_rootx()
y = more_button.winfo_rooty() + more_button.winfo_height()
more_menu.tk_popup(x, y)
def on_sidebar_resize(self, event):
current_width = event.width
threshold_width = 100
if current_width < threshold_width:
for btn, original_text in self.widget_manager.sidebar_buttons:
btn.config(text="", compound="top")
for btn, original_text in self.widget_manager.device_buttons:
btn.config(text="", compound="top")
else:
for btn, original_text in self.widget_manager.sidebar_buttons:
btn.config(text=original_text, compound="left")
for btn, original_text in self.widget_manager.device_buttons:
btn.config(text=original_text, compound="left")
def _on_devices_enter(self, event):
self.widget_manager.devices_scrollbar.grid(
row=1, column=1, sticky="ns")
def _on_devices_leave(self, event):
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()
widget_width = self.widget_manager.devices_canvas.winfo_width()
widget_height = self.widget_manager.devices_canvas.winfo_height()
buffer = 5
if not (widget_x - buffer <= x <= widget_x + widget_width + buffer and
widget_y - buffer <= y <= widget_y + widget_height + buffer):
self.widget_manager.devices_scrollbar.grid_remove()
def toggle_recursive_search(self):
self.widget_manager.recursive_search.set(
not self.widget_manager.recursive_search.get())
if self.widget_manager.recursive_search.get():
self.widget_manager.recursive_button.configure(
style="Header.TButton.Active.Round")
else:
self.widget_manager.recursive_button.configure(
style="Header.TButton.Borderless.Round")
def update_status_bar(self, selected_path=None):
try:
total, used, free = shutil.disk_usage(self.current_dir)
free_str = self._format_size(free)
self.widget_manager.storage_label.config(
text=f"{LocaleStrings.CFD["free_space"]}: {free_str}")
self.widget_manager.storage_bar['value'] = (used / total) * 100
status_text = ""
if selected_path and os.path.exists(selected_path):
if os.path.isdir(selected_path):
content_count = self.view_manager._get_folder_content_count(
selected_path)
if content_count is not None:
status_text = f"'{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()
def on_save(self):
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):
self.selected_file = None
self.destroy()
def get_selected_file(self):
return self.selected_file
def update_action_buttons_state(self):
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):
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):
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, max_len):
return text if len(text) <= max_len else text[:max_len-3] + "..."
def _get_mounted_devices(self):
devices = []
root_disk_name = 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):
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):
if hasattr(self, 'tooltip_window') and self.tooltip_window.winfo_exists():
self.tooltip_window.destroy()