450 lines
20 KiB
Python
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() |