- 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:
21
Changelog
21
Changelog
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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."""
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user