commit 55

This commit is contained in:
2025-08-05 10:14:09 +02:00
parent 3005d17f03
commit f2b6c330fa
7 changed files with 449 additions and 182 deletions

View File

@@ -10,15 +10,22 @@ from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTool
from cfd_app_config import AppConfig, CfdConfigManager
from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir
try:
import send2trash
SEND2TRASH_AVAILABLE = True
except ImportError:
SEND2TRASH_AVAILABLE = False
class SettingsDialog(tk.Toplevel):
def __init__(self, parent):
def __init__(self, parent, dialog_mode="save"):
super().__init__(parent)
self.transient(parent)
self.grab_set()
self.title("Einstellungen")
self.settings = CfdConfigManager.load()
self.dialog_mode = dialog_mode
# Variables
self.search_icon_pos = tk.StringVar(
@@ -29,6 +36,12 @@ class SettingsDialog(tk.Toplevel):
value=self.settings.get("window_size_preset", "1050x850"))
self.default_view_mode = tk.StringVar(
value=self.settings.get("default_view_mode", "icons"))
self.search_hidden_files = tk.BooleanVar(
value=self.settings.get("search_hidden_files", False))
self.use_trash = tk.BooleanVar(
value=self.settings.get("use_trash", False))
self.confirm_delete = tk.BooleanVar(
value=self.settings.get("confirm_delete", False))
# --- UI Elements ---
main_frame = ttk.Frame(self, padding=10)
@@ -70,6 +83,39 @@ class SettingsDialog(tk.Toplevel):
ttk.Radiobutton(view_mode_frame, text="Liste",
variable=self.default_view_mode, value="list").pack(side="left", padx=5)
# Search Hidden Files
search_hidden_frame = ttk.LabelFrame(
main_frame, text="Sucheinstellungen", padding=10)
search_hidden_frame.pack(fill="x", pady=5)
ttk.Checkbutton(search_hidden_frame, text="Versteckte Dateien und Ordner durchsuchen",
variable=self.search_hidden_files).pack(anchor="w")
# Deletion Settings
delete_frame = ttk.LabelFrame(
main_frame, text="Löscheinstellungen", padding=10)
delete_frame.pack(fill="x", pady=5)
self.use_trash_checkbutton = ttk.Checkbutton(delete_frame, text="Dateien in den Papierkorb verschieben (empfohlen)",
variable=self.use_trash)
self.use_trash_checkbutton.pack(anchor="w")
if not SEND2TRASH_AVAILABLE:
self.use_trash_checkbutton.config(state=tk.DISABLED)
ttk.Label(delete_frame, text="(send2trash-Bibliothek nicht gefunden)",
font=("TkDefaultFont", 9, "italic")).pack(anchor="w", padx=(20, 0))
self.confirm_delete_checkbutton = ttk.Checkbutton(delete_frame, text="Löschen/Verschieben ohne Bestätigung",
variable=self.confirm_delete)
self.confirm_delete_checkbutton.pack(anchor="w")
# Disable deletion options in "open" mode
if self.dialog_mode == "open":
self.use_trash_checkbutton.config(state=tk.DISABLED)
self.confirm_delete_checkbutton.config(state=tk.DISABLED)
info_label = ttk.Label(delete_frame, text="(Löschoptionen sind nur im Speichern-Modus verfügbar)",
font=("TkDefaultFont", 9, "italic"))
info_label.pack(anchor="w", padx=(20, 0))
# --- Action Buttons ---
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill="x", pady=(10, 0))
@@ -86,7 +132,10 @@ class SettingsDialog(tk.Toplevel):
"search_icon_pos": self.search_icon_pos.get(),
"button_box_pos": self.button_box_pos.get(),
"window_size_preset": self.window_size_preset.get(),
"default_view_mode": self.default_view_mode.get()
"default_view_mode": self.default_view_mode.get(),
"search_hidden_files": self.search_hidden_files.get(),
"use_trash": self.use_trash.get(),
"confirm_delete": self.confirm_delete.get()
}
CfdConfigManager.save(new_settings)
self.master.reload_config_and_rebuild_ui()
@@ -98,6 +147,9 @@ class SettingsDialog(tk.Toplevel):
self.button_box_pos.set(defaults["button_box_pos"])
self.window_size_preset.set(defaults["window_size_preset"])
self.default_view_mode.set(defaults["default_view_mode"])
self.search_hidden_files.set(defaults["search_hidden_files"])
self.use_trash.set(defaults["use_trash"])
self.confirm_delete.set(defaults["confirm_delete"])
class CustomFileDialog(tk.Toplevel):
@@ -141,13 +193,54 @@ class CustomFileDialog(tk.Toplevel):
self.original_path_text = "" # Store original path text
self.items_to_load_per_batch = 250
self.item_path_map = {}
self.responsive_buttons_hidden = None # State for responsive buttons
self.icon_manager = IconManager()
self.style_manager = StyleManager(self)
self.widget_manager = WidgetManager(self, self.settings)
self._update_view_mode_buttons()
self.navigate_to(self.current_dir)
# Defer initial navigation until the window geometry is calculated
# to ensure the icon view gets the correct initial width.
def initial_load():
# Force layout update to get correct widths
self.update_idletasks()
self.last_width = self.widget_manager.file_list_frame.winfo_width()
self._handle_responsive_buttons(self.winfo_width())
self.navigate_to(self.current_dir)
# Using after(10) gives the window manager a moment to process
# the initial window drawing and sizing.
self.after(10, initial_load)
# Bind the intelligent return handler
self.widget_manager.path_entry.bind("<Return>", self.handle_path_entry_return)
# Bind the delete key only in "save" mode
if self.dialog_mode == "save":
self.bind("<Delete>", self.delete_selected_item)
def handle_path_entry_return(self, event):
"""Intelligently handles the Enter key in the path entry.
If the text is a valid directory, it navigates there.
Otherwise, if in search mode, it executes a search.
"""
path_text = self.widget_manager.path_entry.get().strip()
# Try to interpret as a path first
# Expand user-home and resolve relative paths
potential_path = os.path.realpath(os.path.expanduser(path_text))
if os.path.isdir(potential_path):
# If search was active, turn it off before navigating
if self.search_mode:
self.toggle_search_mode()
self.navigate_to(potential_path)
elif self.search_mode:
# If not a valid path and in search mode, execute search
self.execute_search(event)
def load_settings(self):
self.settings = CfdConfigManager.load()
@@ -179,10 +272,20 @@ class CustomFileDialog(tk.Toplevel):
self.style_manager = StyleManager(self)
self.widget_manager = WidgetManager(self, self.settings)
self._update_view_mode_buttons()
# Reset responsive button state and re-evaluate
self.responsive_buttons_hidden = None
self.update_idletasks()
self._handle_responsive_buttons(self.winfo_width())
# If search was active, reset it to avoid inconsistent state
if self.search_mode:
self.toggle_search_mode() # This will correctly reset the UI
self.navigate_to(self.current_dir)
def open_settings_dialog(self):
SettingsDialog(self)
SettingsDialog(self, dialog_mode=self.dialog_mode)
def get_file_icon(self, filename, size='large'):
ext = os.path.splitext(filename)[1].lower()
@@ -196,7 +299,8 @@ class CustomFileDialog(tk.Toplevel):
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')
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':
@@ -218,12 +322,73 @@ class CustomFileDialog(tk.Toplevel):
self.populate_files()
def on_window_resize(self, event):
new_width = self.widget_manager.file_list_frame.winfo_width()
if self.view_mode.get() == "icons" and abs(new_width - self.last_width) > 50:
if self.resize_job:
self.after_cancel(self.resize_job)
self.resize_job = self.after(200, self.populate_files)
self.last_width = new_width
# This check is to prevent the resize event from firing for child widgets
if event.widget is self:
# Handle icon view redraw on width change, but not in search mode
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():
# Ensure all pending geometry changes are processed before redrawing
self.update_idletasks()
self.populate_files()
self.resize_job = self.after(150, repopulate_icons)
self.last_width = new_width
# Handle responsive buttons in the top bar
self._handle_responsive_buttons(event.width)
def _handle_responsive_buttons(self, window_width):
# This threshold might need adjustment based on your layout and button sizes
threshold = 850
container = self.widget_manager.responsive_buttons_container
more_button = self.widget_manager.more_button
should_be_hidden = window_width < threshold
# Only change the layout if the state is different from the current one
if should_be_hidden != self.responsive_buttons_hidden:
if should_be_hidden:
# Hide individual buttons and show the 'more' button
container.pack_forget()
more_button.pack(side="left", padx=5)
else:
# Show individual buttons and hide the 'more' button
more_button.pack_forget()
container.pack(side="left")
self.responsive_buttons_hidden = should_be_hidden
def show_more_menu(self):
# Create and display the dropdown menu for hidden buttons
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)
more_menu.add_command(label="Neuer Ordner", command=self.create_new_folder,
image=self.icon_manager.get_icon('new_folder_small'), compound='left')
more_menu.add_command(label="Neues Dokument", command=self.create_new_file,
image=self.icon_manager.get_icon('new_document_small'), compound='left')
more_menu.add_separator()
more_menu.add_command(label="Kachelansicht", command=self.set_icon_view,
image=self.icon_manager.get_icon('icon_view'), compound='left')
more_menu.add_command(label="Listenansicht", command=self.set_list_view,
image=self.icon_manager.get_icon('list_view'), compound='left')
more_menu.add_separator()
# Toggle hidden files option
hidden_files_label = "Versteckte Dateien ausblenden" if self.show_hidden_files.get() else "Versteckte Dateien anzeigen"
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.toggle_hidden_files,
image=hidden_files_icon, compound='left')
# Position and show the menu
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
@@ -271,11 +436,10 @@ class CustomFileDialog(tk.Toplevel):
self.original_path_text = self.widget_manager.path_entry.get()
self.widget_manager.path_entry.delete(0, tk.END)
self.widget_manager.path_entry.insert(0, "Suchbegriff eingeben...")
self.widget_manager.path_entry.select_range(0, tk.END)
# Set focus reliably
self.after(50, lambda: self.widget_manager.path_entry.focus_set())
self.widget_manager.path_entry.bind(
"<Return>", self.execute_search)
# Use after() to ensure the focus is set after the UI has updated
self.after(10, lambda: self.widget_manager.path_entry.focus_set())
self.after(20, lambda: self.widget_manager.path_entry.select_range(0, tk.END))
self.widget_manager.path_entry.bind(
"<FocusIn>", self.clear_search_placeholder)
@@ -286,8 +450,6 @@ class CustomFileDialog(tk.Toplevel):
self.search_mode = False
self.widget_manager.path_entry.delete(0, tk.END)
self.widget_manager.path_entry.insert(0, self.original_path_text)
self.widget_manager.path_entry.bind(
"<Return>", lambda e: self.navigate_to(self.widget_manager.path_entry.get()))
self.widget_manager.path_entry.unbind("<FocusIn>")
# Hide search options
@@ -381,11 +543,12 @@ class CustomFileDialog(tk.Toplevel):
# Build find command based on recursive setting (use . for current directory)
if self.widget_manager.recursive_search.get():
find_cmd = ['find', '.', '-iname',
f'*{search_term}*', '-type', 'f']
# Find both files and directories
find_cmd = ['find', '.', '-iname', f'*{search_term}*']
else:
# Find both files and directories, but only in the current level
find_cmd = ['find', '.', '-maxdepth', '1',
'-iname', f'*{search_term}*', '-type', 'f']
'-iname', f'*{search_term}*']
result = subprocess.run(
find_cmd, capture_output=True, text=True, timeout=30)
@@ -398,7 +561,8 @@ class CustomFileDialog(tk.Toplevel):
if f and f.startswith('./'):
abs_path = os.path.join(
search_dir, f[2:]) # Remove './' prefix
if os.path.isfile(abs_path):
# Check if the path exists, as it might be a broken symlink or deleted
if os.path.exists(abs_path):
directory_files.append(abs_path)
all_files.extend(directory_files)
@@ -413,12 +577,21 @@ class CustomFileDialog(tk.Toplevel):
seen.add(file_path)
unique_files.append(file_path)
# Filter based on currently selected filter pattern
# Filter based on currently selected filter pattern and hidden file setting
self.search_results = []
search_hidden = self.settings.get("search_hidden_files", False)
for file_path in unique_files:
filename = os.path.basename(file_path)
if self._matches_filetype(filename):
self.search_results.append(file_path)
# Check if path contains a hidden component (e.g., /.config/ or /some/path/to/.hidden_file)
if not search_hidden:
if any(part.startswith('.') for part in file_path.split(os.sep)):
continue # Skip hidden files/files in hidden directories
# Check if the path exists (it might have been deleted during the search)
if os.path.exists(file_path):
filename = os.path.basename(file_path)
if self._matches_filetype(filename) or os.path.isdir(file_path):
self.search_results.append(file_path)
# Show search results in TreeView
if self.search_results:
@@ -494,7 +667,11 @@ class CustomFileDialog(tk.Toplevel):
modified_time = datetime.fromtimestamp(
stat.st_mtime).strftime('%d.%m.%Y %H:%M')
icon = self.get_file_icon(filename, 'small')
if os.path.isdir(file_path):
icon = self.icon_manager.get_icon('folder_small')
else:
icon = self.get_file_icon(filename, 'small')
search_tree.insert("", "end", text=f" {filename}", image=icon,
values=(directory, size, modified_time))
except (FileNotFoundError, PermissionError):
@@ -675,16 +852,17 @@ class CustomFileDialog(tk.Toplevel):
self.all_items, error, warning = self._get_sorted_items()
self.currently_loaded_count = 0
canvas = tk.Canvas(self.widget_manager.file_list_frame,
highlightthickness=0, bg=self.style_manager.icon_bg_color)
self.icon_canvas = tk.Canvas(self.widget_manager.file_list_frame,
highlightthickness=0, bg=self.style_manager.icon_bg_color)
v_scrollbar = ttk.Scrollbar(
self.widget_manager.file_list_frame, orient="vertical", command=canvas.yview)
canvas.pack(side="left", fill="both", expand=True)
self.widget_manager.file_list_frame, orient="vertical", command=self.icon_canvas.yview)
self.icon_canvas.pack(side="left", fill="both", expand=True)
v_scrollbar.pack(side="right", fill="y")
container_frame = ttk.Frame(canvas, style="Content.TFrame")
canvas.create_window((0, 0), window=container_frame, anchor="nw")
container_frame.bind("<Configure>", lambda e: canvas.configure(
scrollregion=canvas.bbox("all")))
container_frame = ttk.Frame(self.icon_canvas, style="Content.TFrame")
self.icon_canvas.create_window(
(0, 0), window=container_frame, anchor="nw")
container_frame.bind("<Configure>", lambda e: self.icon_canvas.configure(
scrollregion=self.icon_canvas.bbox("all")))
def _on_mouse_wheel(event):
if event.num == 4:
@@ -693,12 +871,12 @@ class CustomFileDialog(tk.Toplevel):
delta = 1
else:
delta = -1 * int(event.delta / 120)
canvas.yview_scroll(delta, "units")
self.icon_canvas.yview_scroll(delta, "units")
# Check if scrolled to the bottom and if there are more items to load
if self.currently_loaded_count < len(self.all_items) and canvas.yview()[1] > 0.9:
if self.currently_loaded_count < len(self.all_items) and self.icon_canvas.yview()[1] > 0.9:
self._load_more_items_icon_view(container_frame)
for widget in [canvas, container_frame]:
for widget in [self.icon_canvas, container_frame]:
widget.bind("<MouseWheel>", _on_mouse_wheel)
widget.bind("<Button-4>", _on_mouse_wheel)
widget.bind("<Button-5>", _on_mouse_wheel)
@@ -709,23 +887,42 @@ class CustomFileDialog(tk.Toplevel):
ttk.Label(container_frame, text=error).pack(pady=20)
return
self._load_more_items_icon_view(
widget_to_focus = self._load_more_items_icon_view(
container_frame, item_to_rename, item_to_select)
if widget_to_focus:
def scroll_to_widget():
self.update_idletasks()
if not widget_to_focus.winfo_exists():
return
y = widget_to_focus.winfo_y()
canvas_height = self.icon_canvas.winfo_height()
scroll_region = self.icon_canvas.bbox("all")
if not scroll_region:
return
scroll_height = scroll_region[3]
if scroll_height > canvas_height:
fraction = y / scroll_height
self.icon_canvas.yview_moveto(fraction)
self.after(100, scroll_to_widget)
def _load_more_items_icon_view(self, container, item_to_rename=None, item_to_select=None):
start_index = self.currently_loaded_count
end_index = min(len(self.all_items), start_index +
self.items_to_load_per_batch)
if start_index >= end_index:
return # All items loaded
return None # All items loaded
item_width, item_height = 125, 100
frame_width = self.widget_manager.file_list_frame.winfo_width()
col_count = max(1, frame_width // item_width - 1)
row = start_index // col_count
col = start_index % col_count
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):
name = self.all_items[i]
@@ -743,6 +940,7 @@ class CustomFileDialog(tk.Toplevel):
if name == item_to_rename:
self.start_rename(item_frame, path)
widget_to_focus = item_frame
else:
icon = self.icon_manager.get_icon(
'folder_large') if is_dir else self.get_file_icon(name, 'large')
@@ -753,12 +951,7 @@ class CustomFileDialog(tk.Toplevel):
name, 14), anchor="center", style="Item.TLabel")
name_label.pack(fill="x", expand=True)
tooltip_text = name
if is_dir and len(self.all_items) < 500:
content_count = self._get_folder_content_count(path)
if content_count is not None:
tooltip_text += f"\n({content_count} Einträge)"
Tooltip(item_frame, tooltip_text)
Tooltip(item_frame, name)
for widget in [item_frame, icon_label, name_label]:
widget.bind("<Double-Button-1>", lambda e,
@@ -772,12 +965,17 @@ class CustomFileDialog(tk.Toplevel):
if name == item_to_select:
self.on_item_select(path, item_frame)
widget_to_focus = item_frame
col = (col + 1) % col_count
if col == 0:
if col_count > 0:
col = (col + 1) % col_count
if col == 0:
row += 1
else:
row += 1
self.currently_loaded_count = end_index
return widget_to_focus
def populate_list_view(self, item_to_rename=None, item_to_select=None):
self.all_items, error, warning = self._get_sorted_items()
@@ -815,7 +1013,8 @@ class CustomFileDialog(tk.Toplevel):
def _on_scroll(*args):
# Check if scrolled to the bottom and if there are more items to load
if self.currently_loaded_count < len(self.all_items) and self.tree.yview()[1] > 0.9:
self._load_more_items_list_view(item_to_rename, item_to_select)
# On-scroll loading should not trigger rename or select.
self._load_more_items_list_view()
v_scrollbar.set(*args)
self.tree.configure(yscrollcommand=_on_scroll)
@@ -886,7 +1085,7 @@ class CustomFileDialog(tk.Toplevel):
child.state(['selected'])
self.selected_item_frame = item_frame
self.selected_file = path
self.update_status_bar()
self.update_status_bar(path) # Pass selected path
self.bind("<F2>", lambda e, p=path,
f=item_frame: self.on_rename_request(e, p, f))
if self.dialog_mode == "save" and not os.path.isdir(path):
@@ -899,8 +1098,9 @@ class CustomFileDialog(tk.Toplevel):
return
item_id = self.tree.selection()[0]
item_text = self.tree.item(item_id, 'text').strip()
self.selected_file = os.path.join(self.current_dir, item_text)
self.update_status_bar()
path = os.path.join(self.current_dir, item_text)
self.selected_file = path
self.update_status_bar(path) # Pass selected path
if self.dialog_mode == "save" and not os.path.isdir(self.selected_file):
self.widget_manager.filename_entry.delete(0, tk.END)
self.widget_manager.filename_entry.insert(0, item_text)
@@ -1006,13 +1206,19 @@ class CustomFileDialog(tk.Toplevel):
self.update_status_bar()
self.update_action_buttons_state()
def go_up_level(self):
"""Navigates one directory level up."""
new_path = os.path.dirname(self.current_dir)
if new_path != self.current_dir: # Avoid getting stuck at the root
self.navigate_to(new_path)
def update_nav_buttons(self):
self.widget_manager.back_button.config(
state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED)
self.widget_manager.forward_button.config(state=tk.NORMAL if self.history_pos < len(
self.history) - 1 else tk.DISABLED)
def update_status_bar(self):
def update_status_bar(self, selected_path=None):
try:
total, used, free = shutil.disk_usage(self.current_dir)
free_str = self._format_size(free)
@@ -1021,10 +1227,19 @@ class CustomFileDialog(tk.Toplevel):
self.widget_manager.storage_bar['value'] = (used / total) * 100
status_text = ""
if self.dialog_mode == "open" and self.selected_file and os.path.exists(self.selected_file) and not os.path.isdir(self.selected_file):
size = os.path.getsize(self.selected_file)
size_str = self._format_size(size)
status_text = f"'{os.path.basename(self.selected_file)}' Größe: {size_str}"
if selected_path and os.path.exists(selected_path):
if os.path.isdir(selected_path):
# Display item count for directories
content_count = self._get_folder_content_count(selected_path)
if content_count is not None:
status_text = f"'{os.path.basename(selected_path)}' ({content_count} Einträge)"
else:
status_text = f"'{os.path.basename(selected_path)}'"
else:
# Display size for files
size = os.path.getsize(selected_path)
size_str = self._format_size(size)
status_text = f"'{os.path.basename(selected_path)}' Größe: {size_str}"
self.widget_manager.status_bar.config(text=status_text)
except FileNotFoundError:
self.widget_manager.status_bar.config(
@@ -1050,6 +1265,48 @@ class CustomFileDialog(tk.Toplevel):
def get_selected_file(self):
return self.selected_file
def delete_selected_item(self, event=None):
"""Deletes or moves the selected item to trash based on settings."""
if not self.selected_file or not os.path.exists(self.selected_file):
return
use_trash = self.settings.get("use_trash", False) and SEND2TRASH_AVAILABLE
confirm = self.settings.get("confirm_delete", False)
action_text = "in den Papierkorb verschieben" if use_trash else "endgültig löschen"
item_name = os.path.basename(self.selected_file)
if not confirm:
dialog = MessageDialog(
master=self,
title="Bestätigung erforderlich",
text=f"Möchten Sie '{item_name}' wirklich {action_text}?",
message_type="question"
)
if not dialog.show():
return
try:
if use_trash:
send2trash.send2trash(self.selected_file)
else:
if os.path.isdir(self.selected_file):
shutil.rmtree(self.selected_file)
else:
os.remove(self.selected_file)
self.populate_files()
self.widget_manager.status_bar.config(
text=f"'{item_name}' wurde erfolgreich entfernt.")
except Exception as e:
MessageDialog(
master=self,
title="Fehler",
text=f"Fehler beim Entfernen von '{item_name}':\n{e}",
message_type="error"
).show()
def create_new_folder(self):
self._create_new_item(is_folder=True)
@@ -1164,7 +1421,19 @@ class CustomFileDialog(tk.Toplevel):
entry.bind("<Escape>", cancel_rename)
def _start_rename_list_view(self, item_id):
x, y, width, height = self.tree.bbox(item_id, column="#0")
# First, ensure the item is visible by scrolling to it.
self.tree.see(item_id)
# Force the UI to process the scrolling and other pending events.
self.tree.update_idletasks()
# Now, get the bounding box. It should be available since the item is visible.
bbox = self.tree.bbox(item_id, column="#0")
# If bbox is still empty (e.g., view is not focused), abort to prevent crash.
if not bbox:
return
x, y, width, height = bbox
entry = ttk.Entry(self.tree)
# Set a fixed width for the entry widget to prevent it from expanding too much
entry_width = self.tree.column("#0", "width")