- Added window on custom_file_dialog to query if there is

no other folder in the selected folder. So that the folder
   can still be entered

 - Fixes multi and dir mode in custom_file_dialog

 - Add "select" in MessageDialog on list for Button and add grab_set()
   after update_idletasks() to fix Error Traceback
This commit is contained in:
2025-08-14 12:59:07 +02:00
parent ff1aede356
commit ba38ea4b87
6 changed files with 181 additions and 75 deletions

View File

@@ -4,6 +4,27 @@ Changelog for shared_libs
-
### Added
14.08.2025
- Added window on custom_file_dialog to query if there is
no other folder in the selected folder. So that the folder
can still be entered
- Fixes multi and dir mode in custom_file_dialog
- Add "select" in MessageDialog on list for Button and add grab_set()
after update_idletasks() to fix Error Traceback
### Added
13.08.2025
- Rename get methode and mode argument in custom_file_dialog
- Add new mode "multi" and "dir" on custom_file_dialog
### Added
12.08.2025

View File

@@ -117,6 +117,11 @@ class LocaleStrings:
"access_to": _("Access to"),
"denied": _("denied."),
"items_selected": _("items selected"),
"select_or_enter_title": _("Select or Enter?"),
"select_or_enter_prompt": _("The folder '{folder_name}' contains no subdirectories. Do you want to select this folder or enter it?"),
"select_button": _("Select"),
"enter_button": _("Enter"),
"cancel_button": _("Cancel"),
}
# Strings from cfd_view_manager.py

View File

@@ -76,10 +76,14 @@ class NavigationManager:
self.dialog.widget_manager.search_animation.stop()
# Clear previous selection state before populating new view
self.dialog.selected_item_frames.clear()
self.dialog.result = None
self.dialog.view_manager.populate_files(
item_to_select=file_to_select)
self.update_nav_buttons()
self.dialog.update_status_bar()
self.dialog.update_selection_info() # Use the new central update method
self.dialog.update_action_buttons_state()
except Exception as e:
self.dialog.widget_manager.search_status_label.config(
@@ -109,7 +113,7 @@ class NavigationManager:
"""Updates all necessary UI components after a navigation action."""
self.dialog.view_manager.populate_files()
self.update_nav_buttons()
self.dialog.update_status_bar()
self.dialog.update_selection_info()
self.dialog.update_action_buttons_state()
def update_nav_buttons(self) -> None:

View File

@@ -10,6 +10,7 @@ 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
@@ -46,7 +47,8 @@ class ViewManager:
self.dialog.widget_manager.path_entry.insert(
0, self.dialog.current_dir)
self.dialog.result = None
self.dialog.update_status_bar()
self.dialog.selected_item_frames.clear() # Clear multi-select frames
self.dialog.update_selection_info() # Use new central method
if self.dialog.view_mode.get() == "list":
self.populate_list_view(item_to_rename, item_to_select)
else:
@@ -283,7 +285,7 @@ class ViewManager:
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))
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,
@@ -437,40 +439,57 @@ class ViewManager:
self.dialog.currently_loaded_count = end_index
return item_found
def on_item_select(self, path: str, item_frame: ttk.Frame) -> None:
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.
Args:
path (str): The path of the selected item.
item_frame: The widget frame of the selected item.
event: The optional tkinter event object.
"""
if self.dialog.dialog_mode == 'dir' and not os.path.isdir(path):
return # Do not allow file selection in dir mode
if self.dialog.dialog_mode == 'multi':
if item_frame in self.dialog.selected_item_frames:
# Deselect
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)
if isinstance(self.dialog.result, list):
self.dialog.result.remove(path)
# Check if Control key is pressed (mask 0x4)
ctrl_pressed = (event.state & 0x4) != 0 if event else False
if ctrl_pressed:
# With CTRL, toggle selection for the clicked item
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:
# Select
# Without CTRL (or programmatic selection), clear previous selection
# and select only the clicked item
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)
if self.dialog.result is None:
self.dialog.result = []
if isinstance(self.dialog.result, list):
self.dialog.result.append(path)
self.dialog.update_status_bar(
f"{len(self.dialog.selected_item_frames)} {LocaleStrings.CFD['items_selected']}")
# Build the list of paths and set it as the result
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'])
@@ -482,21 +501,16 @@ class ViewManager:
if isinstance(child, ttk.Label):
child.state(['selected'])
self.dialog.selected_item_frame = item_frame
self.dialog.result = path
self.dialog.update_status_bar(path)
self.dialog.update_selection_info(path)
self.dialog.search_manager.show_search_ready()
if not os.path.isdir(path) and self.dialog.dialog_mode != 'multi':
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: tk.Event) -> None:
"""Handles the selection of an item in the list view."""
selections = self.dialog.tree.selection()
if not selections:
self.dialog.result = None
self.dialog.update_status_bar()
self.dialog.result = [] if self.dialog.dialog_mode == 'multi' else None
self.dialog.update_selection_info()
return
if self.dialog.dialog_mode == 'multi':
@@ -505,9 +519,8 @@ class ViewManager:
item_text = self.dialog.tree.item(item_id, 'text').strip()
path = os.path.join(self.dialog.current_dir, item_text)
selected_paths.append(path)
self.dialog.result = selected_paths
self.dialog.update_status_bar(
f"{len(selected_paths)} {LocaleStrings.CFD['items_selected']}")
self.dialog.result = selected_paths # Set the result list
self.dialog.update_selection_info() # Call without path
else: # Single selection modes: open, save, dir
item_id = selections[0]
item_text = self.dialog.tree.item(item_id, 'text').strip()
@@ -515,15 +528,12 @@ class ViewManager:
if self.dialog.dialog_mode == 'dir' and not os.path.isdir(path):
self.dialog.result = None
# Deselect the invalid item
self.dialog.tree.selection_remove(item_id)
self.dialog.update_selection_info()
return
self.dialog.result = path
self.dialog.update_status_bar(path)
if not os.path.isdir(path):
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
self.dialog.widget_manager.filename_entry.insert(0, item_text)
# Let the central method handle the result and UI updates
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."""
@@ -546,20 +556,53 @@ class ViewManager:
"""
if os.path.isdir(path):
if self.dialog.dialog_mode == 'dir':
self.dialog.result = path
self.dialog.destroy()
# Check for subdirectories
has_subdirs = False
try:
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:
# Use the existing MessageDialog
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"], # Returns True
LocaleStrings.CFD["enter_button"], # Returns False
LocaleStrings.CFD["cancel_button"], # Returns None
]
)
choice = dialog.show()
if choice is True: # Select
self.dialog.result = path
self.dialog.on_open()
elif choice is False: # Enter
self.dialog.navigation_manager.navigate_to(path)
# If choice is None (Cancel), do nothing
else:
# For all other modes, always navigate
self.dialog.navigation_manager.navigate_to(path)
# This part handles files
elif self.dialog.dialog_mode == "open":
elif self.dialog.dialog_mode in ["open", "multi"]:
self.dialog.result = path
self.dialog.destroy()
self.dialog.on_open() # Use on_open to check file validity
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()
# In 'multi' or 'dir' mode, double-clicking a file does nothing.
def on_list_double_click(self, event: tk.Event) -> None:
"""Handles a double-click on a list view item."""

View File

@@ -418,55 +418,88 @@ class CustomFileDialog(tk.Toplevel):
self.widget_manager.recursive_button.configure(
style="Header.TButton.Borderless.Round")
def update_status_bar(self, status_info: Optional[str] = None) -> None:
def update_selection_info(self, status_info: Optional[str] = None) -> None:
"""
Updates the status bar with disk usage and selected item information.
Updates status bar, filename entry, and result based on current selection.
This is the central handler for selection changes.
"""
self._update_disk_usage()
status_text = ""
Args:
status_info: The path of the currently selected item or a custom string.
"""
# Multi-selection mode
if self.dialog_mode == 'multi':
# The result is now set by the view manager's selection handlers.
# This method is only responsible for the UI update.
selected_paths = self.result if isinstance(self.result, list) else []
self.widget_manager.filename_entry.delete(0, tk.END)
if selected_paths:
filenames = [f'"{os.path.basename(p)}"' for p in selected_paths]
self.widget_manager.filename_entry.insert(0, " ".join(filenames))
count = len(selected_paths)
status_text = f"{count} {LocaleStrings.CFD['items_selected']}"
else:
# Reset when no items are selected
status_text = ""
# Single-selection modes (open, dir, save)
else:
if status_info and os.path.exists(status_info):
self.result = status_info
self.widget_manager.filename_entry.delete(0, tk.END)
self.widget_manager.filename_entry.insert(0, os.path.basename(status_info))
if os.path.isdir(status_info):
content_count = self.view_manager._get_folder_content_count(status_info)
if content_count is not None:
status_text = f"'{os.path.basename(status_info)}' ({content_count} {LocaleStrings.CFD['entries']})"
else:
status_text = f"'{os.path.basename(status_info)}'"
else: # isfile
size = os.path.getsize(status_info)
size_str = self._format_size(size)
status_text = f"'{os.path.basename(status_info)}' {LocaleStrings.VIEW['size']}: {size_str}"
elif status_info: # For custom text
status_text = status_info
self.widget_manager.search_status_label.config(text=status_text)
def _update_disk_usage(self) -> None:
"""Updates only the disk usage part of the status bar."""
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 status_info and os.path.exists(status_info):
selected_path = status_info
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}"
elif status_info:
status_text = status_info
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
self.widget_manager.search_status_label.config(text=LocaleStrings.CFD["directory_not_found"])
def on_open(self) -> None:
"""Handles the 'Open' or 'OK' action based on the dialog mode."""
# For "multi" mode, the result is a list of paths.
if self.dialog_mode == 'multi':
if self.result:
if self.result and isinstance(self.result, list) and self.result:
self.destroy()
elif self.dialog_mode == 'dir':
if self.result and isinstance(self.result, str) and os.path.isdir(self.result):
return
# For single-selection modes, result is a string.
# This now correctly uses self.result which is set by update_selection_info
selected_path = self.result
if not selected_path or not isinstance(selected_path, str):
return # Nothing valid selected
if self.dialog_mode == 'dir':
if os.path.isdir(selected_path):
self.destroy()
elif self.dialog_mode == 'open':
if self.result and isinstance(self.result, str) and os.path.isfile(self.result):
if os.path.isfile(selected_path):
self.destroy()
def on_save(self) -> None:

View File

@@ -123,7 +123,6 @@ class MessageDialog:
self.title = title
# Window creation
self.window = tk.Toplevel(master)
self.window.grab_set()
self.window.resizable(False, False)
ttk.Style().configure("TButton")
self.buttons_widgets = []
@@ -221,6 +220,7 @@ class MessageDialog:
self.window.bind("<Left>", lambda event: self._navigate_left())
self.window.bind("<Right>", lambda event: self._navigate_right())
self.window.update_idletasks()
self.window.grab_set()
self.window.attributes("-alpha", 0.0) # 100% Transparencence
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
self.window.update() # Window update before centering!
@@ -277,7 +277,7 @@ class MessageDialog:
]:
self.result = None
# Check: Button text is "Yes", "Ok", "Continue", "Next", or "Start"
elif button_text.lower() in ["yes", "ok", "continue", "next", "start"]:
elif button_text.lower() in ["yes", "ok", "continue", "next", "start", "select"]:
self.result = True
else:
# Fallback for all other cases (e.g., "No", closing with X, or fewer than 3 buttons)