Compare commits
10 Commits
1d05f5088f
...
cc48f874ac
Author | SHA1 | Date | |
---|---|---|---|
cc48f874ac | |||
27f74e6a77 | |||
ba38ea4b87 | |||
ff1aede356 | |||
f565132074 | |||
d548b545e3 | |||
66202310ec | |||
d79e4c9e01 | |||
0ef94de077 | |||
fbc3c8e051 |
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
|
### Added
|
||||||
12.08.2025
|
12.08.2025
|
||||||
|
|
||||||
|
@@ -471,10 +471,17 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Do not animate if a grab is active on a different window.
|
# Do not animate if a grab is active on a different window.
|
||||||
toplevel = self.winfo_toplevel()
|
try:
|
||||||
grab_widget = toplevel.grab_current()
|
toplevel = self.winfo_toplevel()
|
||||||
if grab_widget is not None and grab_widget != toplevel:
|
grab_widget = toplevel.grab_current()
|
||||||
self.after(100, self._animate) # Check again after a short delay
|
if grab_widget is not None and grab_widget != toplevel:
|
||||||
|
self.after(100, self._animate) # Check again after a short delay
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
# This can happen if a grabbed widget (like a combobox dropdown)
|
||||||
|
# is destroyed at the exact moment this check runs.
|
||||||
|
# It's safest to just skip this animation frame.
|
||||||
|
self.after(30, self._animate)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.angle += 0.1
|
self.angle += 0.1
|
||||||
|
@@ -635,6 +635,8 @@ class IconManager:
|
|||||||
'up': '32/arrow-up.png',
|
'up': '32/arrow-up.png',
|
||||||
'copy': '32/copy.png',
|
'copy': '32/copy.png',
|
||||||
'stair': '32/stair.png',
|
'stair': '32/stair.png',
|
||||||
|
'star': '32/star.png',
|
||||||
|
'connect': '32/connect.png',
|
||||||
'audio_small': '32/audio.png',
|
'audio_small': '32/audio.png',
|
||||||
'icon_view': '32/carrel.png',
|
'icon_view': '32/carrel.png',
|
||||||
'computer_small': '32/computer.png',
|
'computer_small': '32/computer.png',
|
||||||
@@ -695,6 +697,8 @@ class IconManager:
|
|||||||
'up_large': '48/arrow-up.png',
|
'up_large': '48/arrow-up.png',
|
||||||
'copy_large': '48/copy.png',
|
'copy_large': '48/copy.png',
|
||||||
'stair_large': '48/stair.png',
|
'stair_large': '48/stair.png',
|
||||||
|
'star_large': '48/star.png',
|
||||||
|
'connect_large': '48/connect.png',
|
||||||
'icon_view_large': '48/carrel.png',
|
'icon_view_large': '48/carrel.png',
|
||||||
'computer_large': '48/computer.png',
|
'computer_large': '48/computer.png',
|
||||||
'device_large': '48/device.png',
|
'device_large': '48/device.png',
|
||||||
@@ -743,7 +747,9 @@ class IconManager:
|
|||||||
'forward_extralarge': '64/arrow-right.png',
|
'forward_extralarge': '64/arrow-right.png',
|
||||||
'up_extralarge': '64/arrow-up.png',
|
'up_extralarge': '64/arrow-up.png',
|
||||||
'copy_extralarge': '64/copy.png',
|
'copy_extralarge': '64/copy.png',
|
||||||
'stairextralarge': '64/stair.png',
|
'stair_extralarge': '64/stair.png',
|
||||||
|
'star_extralarge': '64/star.png',
|
||||||
|
'connect_extralarge': '64/connect.png',
|
||||||
'audio_large': '64/audio.png',
|
'audio_large': '64/audio.png',
|
||||||
'icon_view_extralarge': '64/carrel.png',
|
'icon_view_extralarge': '64/carrel.png',
|
||||||
'computer_extralarge': '64/computer.png',
|
'computer_extralarge': '64/computer.png',
|
||||||
|
@@ -16,7 +16,8 @@ class CfdConfigManager:
|
|||||||
Manages CFD-specific settings using a JSON file for flexibility.
|
Manages CFD-specific settings using a JSON file for flexibility.
|
||||||
"""
|
"""
|
||||||
# 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year
|
# 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year
|
||||||
VERSION: str = "v. 1.08.1325"
|
UPDATE_URL: str = "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases"
|
||||||
|
VERSION: str = "v. 1.07.0125"
|
||||||
|
|
||||||
MAX_ITEMS_TO_DISPLAY = 1000
|
MAX_ITEMS_TO_DISPLAY = 1000
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ class CfdConfigManager:
|
|||||||
|
|
||||||
_config: Optional[Dict[str, Any]] = None
|
_config: Optional[Dict[str, Any]] = None
|
||||||
_config_file: Path = CONFIG_DIR / "cfd_settings.json"
|
_config_file: Path = CONFIG_DIR / "cfd_settings.json"
|
||||||
|
_bookmarks_file: Path = CONFIG_DIR / "cfd_bookmarks.json"
|
||||||
_default_settings: Dict[str, Any] = {
|
_default_settings: Dict[str, Any] = {
|
||||||
"search_icon_pos": "left", # 'left' or 'right'
|
"search_icon_pos": "left", # 'left' or 'right'
|
||||||
"button_box_pos": "left", # 'left' or 'right'
|
"button_box_pos": "left", # 'left' or 'right'
|
||||||
@@ -44,7 +46,8 @@ class CfdConfigManager:
|
|||||||
"use_trash": False, # True or False
|
"use_trash": False, # True or False
|
||||||
"confirm_delete": False, # True or False
|
"confirm_delete": False, # True or False
|
||||||
"recursive_search": True,
|
"recursive_search": True,
|
||||||
"use_pillow_animation": True
|
"use_pillow_animation": True,
|
||||||
|
"keep_bookmarks_on_reset": True # Keep bookmarks when resetting settings
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -83,6 +86,51 @@ class CfdConfigManager:
|
|||||||
except IOError as e:
|
except IOError as e:
|
||||||
print(f"Error saving settings: {e}")
|
print(f"Error saving settings: {e}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ensure_bookmarks_file(cls: Type['CfdConfigManager']) -> None:
|
||||||
|
"""Ensures the bookmarks file exists."""
|
||||||
|
if not cls._bookmarks_file.exists():
|
||||||
|
try:
|
||||||
|
cls._bookmarks_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(cls._bookmarks_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump({}, f, indent=4)
|
||||||
|
except IOError as e:
|
||||||
|
print(f"Error creating bookmarks file: {e}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_bookmarks(cls: Type['CfdConfigManager']) -> Dict[str, Any]:
|
||||||
|
"""Loads bookmarks from the JSON file."""
|
||||||
|
cls._ensure_bookmarks_file()
|
||||||
|
try:
|
||||||
|
with open(cls._bookmarks_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (IOError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save_bookmarks(cls: Type['CfdConfigManager'], bookmarks: Dict[str, Any]) -> None:
|
||||||
|
"""Saves the given bookmarks dictionary to the JSON file."""
|
||||||
|
try:
|
||||||
|
with open(cls._bookmarks_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(bookmarks, f, indent=4)
|
||||||
|
except IOError as e:
|
||||||
|
print(f"Error saving bookmarks: {e}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_bookmark(cls: Type['CfdConfigManager'], name: str, data: Dict[str, Any]) -> None:
|
||||||
|
"""Adds or updates a bookmark."""
|
||||||
|
bookmarks = cls.load_bookmarks()
|
||||||
|
bookmarks[name] = data
|
||||||
|
cls.save_bookmarks(bookmarks)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove_bookmark(cls: Type['CfdConfigManager'], name: str) -> None:
|
||||||
|
"""Removes a bookmark by name."""
|
||||||
|
bookmarks = cls.load_bookmarks()
|
||||||
|
if name in bookmarks:
|
||||||
|
del bookmarks[name]
|
||||||
|
cls.save_bookmarks(bookmarks)
|
||||||
|
|
||||||
|
|
||||||
class LocaleStrings:
|
class LocaleStrings:
|
||||||
"""
|
"""
|
||||||
@@ -115,6 +163,12 @@ class LocaleStrings:
|
|||||||
"not_found": _("not found."),
|
"not_found": _("not found."),
|
||||||
"access_to": _("Access to"),
|
"access_to": _("Access to"),
|
||||||
"denied": _("denied."),
|
"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
|
# Strings from cfd_view_manager.py
|
||||||
@@ -160,6 +214,10 @@ class LocaleStrings:
|
|||||||
"no_results_for": _("No results for"),
|
"no_results_for": _("No results for"),
|
||||||
"error_during_search": _("Error during search"),
|
"error_during_search": _("Error during search"),
|
||||||
"search_error": _("Search Error"),
|
"search_error": _("Search Error"),
|
||||||
|
"install_new_version": _("Install new version {version}"),
|
||||||
|
"sftp_connection": _("SFTP Connection"),
|
||||||
|
"sftp_bookmarks": _("SFTP Bookmarks"),
|
||||||
|
"remove_bookmark": _("Remove Bookmark"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Strings from cfd_settings_dialog.py
|
# Strings from cfd_settings_dialog.py
|
||||||
@@ -194,6 +252,11 @@ class LocaleStrings:
|
|||||||
"blink": _("Blink"),
|
"blink": _("Blink"),
|
||||||
"deletion_options_info": _("Deletion options are only available in save mode"),
|
"deletion_options_info": _("Deletion options are only available in save mode"),
|
||||||
"reset_to_default": _("Reset to Default"),
|
"reset_to_default": _("Reset to Default"),
|
||||||
|
"sftp_settings": _("SFTP Settings"),
|
||||||
|
"paramiko_not_found": _("Paramiko library not found."),
|
||||||
|
"sftp_disabled": _("SFTP functionality is disabled. Please install 'paramiko'."),
|
||||||
|
"paramiko_found": _("Paramiko library found. SFTP is enabled."),
|
||||||
|
"keep_sftp_bookmarks": _("Keep SFTP bookmarks on reset"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Strings from cfd_file_operations.py
|
# Strings from cfd_file_operations.py
|
||||||
|
@@ -41,7 +41,45 @@ class FileOperationsManager:
|
|||||||
Args:
|
Args:
|
||||||
event: The event that triggered the deletion (optional).
|
event: The event that triggered the deletion (optional).
|
||||||
"""
|
"""
|
||||||
if not self.dialog.selected_file or not os.path.exists(self.dialog.selected_file):
|
if not self.dialog.result or not isinstance(self.dialog.result, str):
|
||||||
|
return
|
||||||
|
|
||||||
|
selected_path = self.dialog.result
|
||||||
|
|
||||||
|
if self.dialog.current_fs_type == 'sftp':
|
||||||
|
item_name = os.path.basename(selected_path)
|
||||||
|
dialog = MessageDialog(
|
||||||
|
master=self.dialog,
|
||||||
|
title=LocaleStrings.FILE["confirm_delete_title"],
|
||||||
|
text=f"{LocaleStrings.FILE['are_you_sure']} '{item_name}' {LocaleStrings.FILE['delete_permanently']}?",
|
||||||
|
message_type="question"
|
||||||
|
)
|
||||||
|
if not dialog.show():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.dialog.sftp_manager.path_is_dir(selected_path):
|
||||||
|
success, msg = self.dialog.sftp_manager.rm_recursive(selected_path)
|
||||||
|
else:
|
||||||
|
success, msg = self.dialog.sftp_manager.rm(selected_path)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
self.dialog.view_manager.populate_files()
|
||||||
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
|
text=f"'{item_name}' {LocaleStrings.FILE['was_successfully_removed']}")
|
||||||
|
except Exception as e:
|
||||||
|
MessageDialog(
|
||||||
|
master=self.dialog,
|
||||||
|
title=LocaleStrings.FILE["error_title"],
|
||||||
|
text=f"{LocaleStrings.FILE['error_removing']} '{item_name}':\n{e}",
|
||||||
|
message_type="error"
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Local deletion logic
|
||||||
|
if not os.path.exists(selected_path):
|
||||||
return
|
return
|
||||||
|
|
||||||
use_trash = self.dialog.settings.get(
|
use_trash = self.dialog.settings.get(
|
||||||
@@ -49,7 +87,7 @@ class FileOperationsManager:
|
|||||||
confirm = self.dialog.settings.get("confirm_delete", False)
|
confirm = self.dialog.settings.get("confirm_delete", False)
|
||||||
|
|
||||||
action_text = LocaleStrings.FILE["move_to_trash"] if use_trash else LocaleStrings.FILE["delete_permanently"]
|
action_text = LocaleStrings.FILE["move_to_trash"] if use_trash else LocaleStrings.FILE["delete_permanently"]
|
||||||
item_name = os.path.basename(self.dialog.selected_file)
|
item_name = os.path.basename(selected_path)
|
||||||
|
|
||||||
if not confirm:
|
if not confirm:
|
||||||
dialog = MessageDialog(
|
dialog = MessageDialog(
|
||||||
@@ -63,12 +101,12 @@ class FileOperationsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if use_trash:
|
if use_trash:
|
||||||
send2trash.send2trash(self.dialog.selected_file)
|
send2trash.send2trash(selected_path)
|
||||||
else:
|
else:
|
||||||
if os.path.isdir(self.dialog.selected_file):
|
if os.path.isdir(selected_path):
|
||||||
shutil.rmtree(self.dialog.selected_file)
|
shutil.rmtree(selected_path)
|
||||||
else:
|
else:
|
||||||
os.remove(self.dialog.selected_file)
|
os.remove(selected_path)
|
||||||
|
|
||||||
self.dialog.view_manager.populate_files()
|
self.dialog.view_manager.populate_files()
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
@@ -104,10 +142,20 @@ class FileOperationsManager:
|
|||||||
new_path = os.path.join(self.dialog.current_dir, new_name)
|
new_path = os.path.join(self.dialog.current_dir, new_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_folder:
|
if self.dialog.current_fs_type == 'sftp':
|
||||||
os.mkdir(new_path)
|
if is_folder:
|
||||||
|
success, msg = self.dialog.sftp_manager.mkdir(new_path)
|
||||||
|
if not success:
|
||||||
|
raise OSError(msg)
|
||||||
|
else:
|
||||||
|
success, msg = self.dialog.sftp_manager.touch(new_path)
|
||||||
|
if not success:
|
||||||
|
raise OSError(msg)
|
||||||
else:
|
else:
|
||||||
open(new_path, 'a').close()
|
if is_folder:
|
||||||
|
os.mkdir(new_path)
|
||||||
|
else:
|
||||||
|
open(new_path, 'a').close()
|
||||||
self.dialog.view_manager.populate_files(item_to_rename=new_name)
|
self.dialog.view_manager.populate_files(item_to_rename=new_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
@@ -129,7 +177,10 @@ class FileOperationsManager:
|
|||||||
name, ext = os.path.splitext(base_name)
|
name, ext = os.path.splitext(base_name)
|
||||||
counter = 1
|
counter = 1
|
||||||
new_name = base_name
|
new_name = base_name
|
||||||
while os.path.exists(os.path.join(self.dialog.current_dir, new_name)):
|
|
||||||
|
path_exists = self.dialog.sftp_manager.exists if self.dialog.current_fs_type == 'sftp' else os.path.exists
|
||||||
|
|
||||||
|
while path_exists(os.path.join(self.dialog.current_dir, new_name)):
|
||||||
counter += 1
|
counter += 1
|
||||||
new_name = f"{name} {counter}{ext}"
|
new_name = f"{name} {counter}{ext}"
|
||||||
return new_name
|
return new_name
|
||||||
@@ -327,16 +378,32 @@ class FileOperationsManager:
|
|||||||
self.dialog.view_manager.populate_files(item_to_select=old_name)
|
self.dialog.view_manager.populate_files(item_to_select=old_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
if os.path.exists(new_path):
|
if self.dialog.current_fs_type == 'sftp':
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
if self.dialog.sftp_manager.exists(new_path):
|
||||||
text=f"'{new_name}' {LocaleStrings.FILE['folder_exists_error']}")
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
self.dialog.view_manager.populate_files(item_to_select=old_name)
|
text=f"'{new_name}' {LocaleStrings.FILE['folder_exists_error']}")
|
||||||
return
|
self.dialog.view_manager.populate_files(item_to_select=old_name)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
success, msg = self.dialog.sftp_manager.rename(old_path, new_path)
|
||||||
|
if not success:
|
||||||
|
raise OSError(msg)
|
||||||
|
self.dialog.view_manager.populate_files(item_to_select=new_name)
|
||||||
|
except Exception as e:
|
||||||
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
|
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
|
||||||
|
self.dialog.view_manager.populate_files()
|
||||||
|
else:
|
||||||
|
if os.path.exists(new_path):
|
||||||
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
|
text=f"'{new_name}' {LocaleStrings.FILE['folder_exists_error']}")
|
||||||
|
self.dialog.view_manager.populate_files(item_to_select=old_name)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.rename(old_path, new_path)
|
os.rename(old_path, new_path)
|
||||||
self.dialog.view_manager.populate_files(item_to_select=new_name)
|
self.dialog.view_manager.populate_files(item_to_select=new_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
|
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
|
||||||
self.dialog.view_manager.populate_files()
|
self.dialog.view_manager.populate_files()
|
@@ -76,10 +76,14 @@ class NavigationManager:
|
|||||||
|
|
||||||
self.dialog.widget_manager.search_animation.stop()
|
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(
|
self.dialog.view_manager.populate_files(
|
||||||
item_to_select=file_to_select)
|
item_to_select=file_to_select)
|
||||||
self.update_nav_buttons()
|
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()
|
self.dialog.update_action_buttons_state()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
@@ -101,15 +105,22 @@ class NavigationManager:
|
|||||||
|
|
||||||
def go_up_level(self) -> None:
|
def go_up_level(self) -> None:
|
||||||
"""Navigates to the parent directory of the current directory."""
|
"""Navigates to the parent directory of the current directory."""
|
||||||
new_path = os.path.dirname(self.dialog.current_dir)
|
if self.dialog.current_fs_type == "sftp":
|
||||||
if new_path != self.dialog.current_dir:
|
if self.dialog.current_dir and self.dialog.current_dir != "/":
|
||||||
self.navigate_to(new_path)
|
new_path = self.dialog.current_dir.rsplit('/', 1)[0]
|
||||||
|
if not new_path:
|
||||||
|
new_path = "/"
|
||||||
|
self.navigate_to(new_path)
|
||||||
|
else:
|
||||||
|
new_path = os.path.dirname(self.dialog.current_dir)
|
||||||
|
if new_path != self.dialog.current_dir:
|
||||||
|
self.navigate_to(new_path)
|
||||||
|
|
||||||
def _update_ui_after_navigation(self) -> None:
|
def _update_ui_after_navigation(self) -> None:
|
||||||
"""Updates all necessary UI components after a navigation action."""
|
"""Updates all necessary UI components after a navigation action."""
|
||||||
self.dialog.view_manager.populate_files()
|
self.dialog.view_manager.populate_files()
|
||||||
self.update_nav_buttons()
|
self.update_nav_buttons()
|
||||||
self.dialog.update_status_bar()
|
self.dialog.update_selection_info()
|
||||||
self.dialog.update_action_buttons_state()
|
self.dialog.update_action_buttons_state()
|
||||||
|
|
||||||
def update_nav_buttons(self) -> None:
|
def update_nav_buttons(self) -> None:
|
||||||
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
||||||
from shared_libs.animated_icon import PIL_AVAILABLE
|
from shared_libs.animated_icon import PIL_AVAILABLE
|
||||||
|
from .cfd_sftp_manager import PARAMIKO_AVAILABLE
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
@@ -56,6 +57,8 @@ class SettingsDialog(tk.Toplevel):
|
|||||||
value=self.settings.get("use_pillow_animation", False))
|
value=self.settings.get("use_pillow_animation", False))
|
||||||
self.animation_type = tk.StringVar(
|
self.animation_type = tk.StringVar(
|
||||||
value=self.settings.get("animation_type", "double_arc"))
|
value=self.settings.get("animation_type", "double_arc"))
|
||||||
|
self.keep_bookmarks_on_reset = tk.BooleanVar(
|
||||||
|
value=self.settings.get("keep_bookmarks_on_reset", True))
|
||||||
|
|
||||||
# --- UI Elements ---
|
# --- UI Elements ---
|
||||||
main_frame = ttk.Frame(self, padding=10)
|
main_frame = ttk.Frame(self, padding=10)
|
||||||
@@ -140,6 +143,24 @@ class SettingsDialog(tk.Toplevel):
|
|||||||
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["blink"], variable=self.animation_type,
|
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["blink"], variable=self.animation_type,
|
||||||
value="blink").pack(side="left", padx=5)
|
value="blink").pack(side="left", padx=5)
|
||||||
|
|
||||||
|
# SFTP Settings
|
||||||
|
sftp_frame = ttk.LabelFrame(
|
||||||
|
main_frame, text=LocaleStrings.SET["sftp_settings"], padding=10)
|
||||||
|
sftp_frame.pack(fill="x", pady=5)
|
||||||
|
|
||||||
|
if not PARAMIKO_AVAILABLE:
|
||||||
|
ttk.Label(sftp_frame, text=LocaleStrings.SET["paramiko_not_found"],
|
||||||
|
font=("TkDefaultFont", 9, "italic")).pack(anchor="w")
|
||||||
|
ttk.Label(sftp_frame, text=LocaleStrings.SET["sftp_disabled"],
|
||||||
|
font=("TkDefaultFont", 9, "italic")).pack(anchor="w")
|
||||||
|
else:
|
||||||
|
ttk.Label(sftp_frame, text=LocaleStrings.SET["paramiko_found"],
|
||||||
|
font=("TkDefaultFont", 9)).pack(anchor="w")
|
||||||
|
|
||||||
|
self.keep_bookmarks_checkbutton = ttk.Checkbutton(sftp_frame, text=LocaleStrings.SET["keep_sftp_bookmarks"],
|
||||||
|
variable=self.keep_bookmarks_on_reset)
|
||||||
|
self.keep_bookmarks_checkbutton.pack(anchor="w", pady=(5,0))
|
||||||
|
|
||||||
# Disable deletion options in "open" mode
|
# Disable deletion options in "open" mode
|
||||||
if not self.dialog_mode == "save":
|
if not self.dialog_mode == "save":
|
||||||
self.use_trash_checkbutton.config(state=tk.DISABLED)
|
self.use_trash_checkbutton.config(state=tk.DISABLED)
|
||||||
@@ -174,7 +195,8 @@ class SettingsDialog(tk.Toplevel):
|
|||||||
"use_trash": self.use_trash.get(),
|
"use_trash": self.use_trash.get(),
|
||||||
"confirm_delete": self.confirm_delete.get(),
|
"confirm_delete": self.confirm_delete.get(),
|
||||||
"use_pillow_animation": self.use_pillow_animation.get(),
|
"use_pillow_animation": self.use_pillow_animation.get(),
|
||||||
"animation_type": self.animation_type.get()
|
"animation_type": self.animation_type.get(),
|
||||||
|
"keep_bookmarks_on_reset": self.keep_bookmarks_on_reset.get()
|
||||||
}
|
}
|
||||||
CfdConfigManager.save(new_settings)
|
CfdConfigManager.save(new_settings)
|
||||||
self.master.reload_config_and_rebuild_ui()
|
self.master.reload_config_and_rebuild_ui()
|
||||||
@@ -193,3 +215,4 @@ class SettingsDialog(tk.Toplevel):
|
|||||||
self.use_pillow_animation.set(defaults.get(
|
self.use_pillow_animation.set(defaults.get(
|
||||||
"use_pillow_animation", True) and PIL_AVAILABLE)
|
"use_pillow_animation", True) and PIL_AVAILABLE)
|
||||||
self.animation_type.set(defaults.get("animation_type", "counter_arc"))
|
self.animation_type.set(defaults.get("animation_type", "counter_arc"))
|
||||||
|
self.keep_bookmarks_on_reset.set(defaults.get("keep_bookmarks_on_reset", True))
|
151
custom_file_dialog/cfd_sftp_manager.py
Normal file
151
custom_file_dialog/cfd_sftp_manager.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# cfd_sftp_manager.py
|
||||||
|
|
||||||
|
try:
|
||||||
|
import paramiko
|
||||||
|
PARAMIKO_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
paramiko = None
|
||||||
|
PARAMIKO_AVAILABLE = False
|
||||||
|
|
||||||
|
class SFTPManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.client = None
|
||||||
|
self.sftp = None
|
||||||
|
|
||||||
|
def connect(self, host, port, username, password=None, key_file=None, passphrase=None):
|
||||||
|
if not PARAMIKO_AVAILABLE:
|
||||||
|
raise ImportError("Paramiko library is not installed. SFTP functionality is disabled.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client = paramiko.SSHClient()
|
||||||
|
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
self.client.connect(
|
||||||
|
hostname=host,
|
||||||
|
port=port,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
key_filename=key_file,
|
||||||
|
passphrase=passphrase,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
self.sftp = self.client.open_sftp()
|
||||||
|
return True, "Connection successful."
|
||||||
|
except Exception as e:
|
||||||
|
self.client = None
|
||||||
|
self.sftp = None
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if self.sftp:
|
||||||
|
self.sftp.close()
|
||||||
|
self.sftp = None
|
||||||
|
if self.client:
|
||||||
|
self.client.close()
|
||||||
|
self.client = None
|
||||||
|
|
||||||
|
def list_directory(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return [], "Not connected."
|
||||||
|
|
||||||
|
try:
|
||||||
|
items = self.sftp.listdir_attr(path)
|
||||||
|
# Sort directories first, then files
|
||||||
|
items.sort(key=lambda x: (not self.item_is_dir(x), x.filename.lower()))
|
||||||
|
return items, None
|
||||||
|
except Exception as e:
|
||||||
|
return [], str(e)
|
||||||
|
|
||||||
|
def item_is_dir(self, item):
|
||||||
|
# Helper to check if an SFTP attribute object is a directory
|
||||||
|
import stat
|
||||||
|
return stat.S_ISDIR(item.st_mode)
|
||||||
|
|
||||||
|
def path_is_dir(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
import stat
|
||||||
|
return stat.S_ISDIR(self.sftp.stat(path).st_mode)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def exists(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self.sftp.stat(path)
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mkdir(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False, "Not connected."
|
||||||
|
try:
|
||||||
|
self.sftp.mkdir(path)
|
||||||
|
return True, ""
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def rmdir(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False, "Not connected."
|
||||||
|
try:
|
||||||
|
self.sftp.rmdir(path)
|
||||||
|
return True, ""
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def rm(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False, "Not connected."
|
||||||
|
try:
|
||||||
|
self.sftp.remove(path)
|
||||||
|
return True, ""
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def rename(self, old_path, new_path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False, "Not connected."
|
||||||
|
try:
|
||||||
|
self.sftp.rename(old_path, new_path)
|
||||||
|
return True, ""
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def touch(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False, "Not connected."
|
||||||
|
try:
|
||||||
|
with self.sftp.open(path, 'w') as f:
|
||||||
|
pass
|
||||||
|
return True, ""
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def rm_recursive(self, path):
|
||||||
|
if not self.sftp:
|
||||||
|
return False, "Not connected."
|
||||||
|
|
||||||
|
try:
|
||||||
|
items = self.sftp.listdir_attr(path)
|
||||||
|
for item in items:
|
||||||
|
remote_path = f"{path}/{item.filename}"
|
||||||
|
if self.item_is_dir(item):
|
||||||
|
success, msg = self.rm_recursive(remote_path)
|
||||||
|
if not success:
|
||||||
|
return False, msg
|
||||||
|
else:
|
||||||
|
self.sftp.remove(remote_path)
|
||||||
|
self.sftp.rmdir(path)
|
||||||
|
return True, ""
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self):
|
||||||
|
return self.sftp is not None
|
@@ -10,21 +10,14 @@ if TYPE_CHECKING:
|
|||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
|
|
||||||
from shared_libs.common_tools import Tooltip
|
from shared_libs.common_tools import Tooltip
|
||||||
from shared_libs.animated_icon import AnimatedIcon
|
from shared_libs.animated_icon import AnimatedIcon, PIL_AVAILABLE
|
||||||
from .cfd_app_config import LocaleStrings
|
from .cfd_app_config import LocaleStrings
|
||||||
|
from .cfd_sftp_manager import PARAMIKO_AVAILABLE
|
||||||
|
|
||||||
|
|
||||||
def get_xdg_user_dir(dir_key: str, fallback_name: str) -> str:
|
def get_xdg_user_dir(dir_key: str, fallback_name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieves a user directory path from the XDG user-dirs.dirs config file.
|
Retrieves a user directory path from the XDG user-dirs.dirs config file.
|
||||||
|
|
||||||
Args:
|
|
||||||
dir_key (str): The key for the directory (e.g., "XDG_DOWNLOAD_DIR").
|
|
||||||
fallback_name (str): The name of the directory to use as a fallback
|
|
||||||
if the key is not found (e.g., "Downloads").
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The absolute path to the user directory.
|
|
||||||
"""
|
"""
|
||||||
home = os.path.expanduser("~")
|
home = os.path.expanduser("~")
|
||||||
fallback_path = os.path.join(home, fallback_name)
|
fallback_path = os.path.join(home, fallback_name)
|
||||||
@@ -52,19 +45,10 @@ class StyleManager:
|
|||||||
"""Manages the visual styling of the application using ttk styles."""
|
"""Manages the visual styling of the application using ttk styles."""
|
||||||
|
|
||||||
def __init__(self, dialog: 'CustomFileDialog') -> None:
|
def __init__(self, dialog: 'CustomFileDialog') -> None:
|
||||||
"""
|
|
||||||
Initializes the StyleManager.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dialog: The main CustomFileDialog instance.
|
|
||||||
"""
|
|
||||||
self.dialog = dialog
|
self.dialog = dialog
|
||||||
self.setup_styles()
|
self.setup_styles()
|
||||||
|
|
||||||
def setup_styles(self) -> None:
|
def setup_styles(self) -> None:
|
||||||
"""
|
|
||||||
Configures all the ttk styles for the dialog based on a light or dark theme.
|
|
||||||
"""
|
|
||||||
style = ttk.Style(self.dialog)
|
style = ttk.Style(self.dialog)
|
||||||
base_bg = self.dialog.cget('background')
|
base_bg = self.dialog.cget('background')
|
||||||
self.is_dark = sum(self.dialog.winfo_rgb(base_bg)) / 3 < 32768
|
self.is_dark = sum(self.dialog.winfo_rgb(base_bg)) / 3 < 32768
|
||||||
@@ -129,40 +113,29 @@ class StyleManager:
|
|||||||
('selected', "black" if not self.is_dark else "white")])
|
('selected', "black" if not self.is_dark else "white")])
|
||||||
style.configure("TButton.Borderless.Round", anchor="w")
|
style.configure("TButton.Borderless.Round", anchor="w")
|
||||||
style.configure("Small.Horizontal.TProgressbar", thickness=8)
|
style.configure("Small.Horizontal.TProgressbar", thickness=8)
|
||||||
|
|
||||||
style.configure("Bottom.TButton.Borderless.Round",
|
style.configure("Bottom.TButton.Borderless.Round",
|
||||||
background=self.bottom_color)
|
background=self.bottom_color)
|
||||||
style.map("Bottom.TButton.Borderless.Round",
|
style.map("Bottom.TButton.Borderless.Round", background=[
|
||||||
background=[('active', self.hover_extrastyle)])
|
('active', self.hover_extrastyle)])
|
||||||
style.layout("Bottom.TButton.Borderless.Round",
|
style.layout("Bottom.TButton.Borderless.Round",
|
||||||
style.layout("Header.TButton.Borderless.Round")
|
style.layout("Header.TButton.Borderless.Round"))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WidgetManager:
|
class WidgetManager:
|
||||||
"""Manages the creation, layout, and management of all widgets in the dialog."""
|
"""Manages the creation, layout, and management of all widgets in the dialog."""
|
||||||
|
|
||||||
def __init__(self, dialog: 'CustomFileDialog', settings: Dict[str, Any]) -> None:
|
def __init__(self, dialog: 'CustomFileDialog', settings: Dict[str, Any]) -> None:
|
||||||
"""
|
|
||||||
Initializes the WidgetManager.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dialog: The main CustomFileDialog instance.
|
|
||||||
settings (dict): The application settings.
|
|
||||||
"""
|
|
||||||
self.dialog = dialog
|
self.dialog = dialog
|
||||||
self.style_manager = dialog.style_manager
|
self.style_manager = dialog.style_manager
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.setup_widgets()
|
self.setup_widgets()
|
||||||
|
|
||||||
def _setup_top_bar(self, parent_frame: ttk.Frame) -> None:
|
def _setup_top_bar(self, parent_frame: ttk.Frame) -> None:
|
||||||
"""Sets up the top bar with navigation and control buttons."""
|
|
||||||
top_bar = ttk.Frame(
|
top_bar = ttk.Frame(
|
||||||
parent_frame, style='Accent.TFrame', padding=(0, 5, 0, 5))
|
parent_frame, style='Accent.TFrame', padding=(0, 5, 0, 5))
|
||||||
top_bar.grid(row=0, column=0, columnspan=2, sticky="ew")
|
top_bar.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||||
top_bar.grid_columnconfigure(1, weight=1)
|
top_bar.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# Left navigation
|
|
||||||
left_nav_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
left_nav_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
||||||
left_nav_container.grid(row=0, column=0, sticky="w")
|
left_nav_container.grid(row=0, column=0, sticky="w")
|
||||||
left_nav_container.grid_propagate(False)
|
left_nav_container.grid_propagate(False)
|
||||||
@@ -187,23 +160,29 @@ class WidgetManager:
|
|||||||
self.home_button.pack(side="left", padx=(5, 10))
|
self.home_button.pack(side="left", padx=(5, 10))
|
||||||
Tooltip(self.home_button, LocaleStrings.UI["home"])
|
Tooltip(self.home_button, LocaleStrings.UI["home"])
|
||||||
|
|
||||||
# Path and search
|
|
||||||
path_search_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
path_search_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
||||||
path_search_container.grid(row=0, column=1, sticky="ew")
|
path_search_container.grid(row=0, column=1, sticky="ew")
|
||||||
|
path_search_container.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self.path_entry = ttk.Entry(path_search_container)
|
self.path_entry = ttk.Entry(path_search_container)
|
||||||
|
self.path_entry.grid(row=0, column=0, sticky="ew")
|
||||||
self.path_entry.bind(
|
self.path_entry.bind(
|
||||||
"<Return>", lambda e: self.dialog.navigation_manager.navigate_to(self.path_entry.get()))
|
"<Return>", lambda e: self.dialog.navigation_manager.navigate_to(self.path_entry.get()))
|
||||||
|
|
||||||
search_icon_pos = self.settings.get("search_icon_pos", "left")
|
self.update_animation_icon = AnimatedIcon(
|
||||||
if search_icon_pos == 'left':
|
path_search_container,
|
||||||
path_search_container.grid_columnconfigure(1, weight=1)
|
width=20,
|
||||||
self.path_entry.grid(row=0, column=1, sticky="ew")
|
height=20,
|
||||||
else: # right
|
animation_type="blink",
|
||||||
path_search_container.grid_columnconfigure(0, weight=1)
|
use_pillow=PIL_AVAILABLE,
|
||||||
self.path_entry.grid(row=0, column=0, sticky="ew")
|
bg=self.style_manager.header
|
||||||
|
)
|
||||||
|
self.update_animation_icon.grid(
|
||||||
|
row=0, column=1, sticky='e', padx=(5, 0))
|
||||||
|
self.update_animation_icon.grid_remove() # Initially hidden
|
||||||
|
|
||||||
# Right controls
|
right_controls_container = ttk.Frame(
|
||||||
right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
top_bar, style='Accent.TFrame')
|
||||||
right_controls_container.grid(row=0, column=2, sticky="e")
|
right_controls_container.grid(row=0, column=2, sticky="e")
|
||||||
self.responsive_buttons_container = ttk.Frame(
|
self.responsive_buttons_container = ttk.Frame(
|
||||||
right_controls_container, style='Accent.TFrame')
|
right_controls_container, style='Accent.TFrame')
|
||||||
@@ -219,6 +198,19 @@ class WidgetManager:
|
|||||||
self.new_file_button.pack(side="left", padx=5)
|
self.new_file_button.pack(side="left", padx=5)
|
||||||
Tooltip(self.new_file_button, LocaleStrings.UI["new_document"])
|
Tooltip(self.new_file_button, LocaleStrings.UI["new_document"])
|
||||||
|
|
||||||
|
sftp_icon = self.dialog.icon_manager.get_icon('connect')
|
||||||
|
if sftp_icon:
|
||||||
|
self.sftp_button = ttk.Button(self.responsive_buttons_container, image=sftp_icon,
|
||||||
|
command=self.dialog.open_sftp_dialog, style="Header.TButton.Borderless.Round")
|
||||||
|
else:
|
||||||
|
self.sftp_button = ttk.Button(self.responsive_buttons_container, text="SFTP",
|
||||||
|
command=self.dialog.open_sftp_dialog, style="Header.TButton.Borderless.Round")
|
||||||
|
|
||||||
|
self.sftp_button.pack(side="left", padx=5)
|
||||||
|
Tooltip(self.sftp_button, LocaleStrings.UI["sftp_connection"])
|
||||||
|
if not PARAMIKO_AVAILABLE:
|
||||||
|
self.sftp_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
if self.dialog.dialog_mode == "open":
|
if self.dialog.dialog_mode == "open":
|
||||||
self.new_folder_button.config(state=tk.DISABLED)
|
self.new_folder_button.config(state=tk.DISABLED)
|
||||||
self.new_file_button.config(state=tk.DISABLED)
|
self.new_file_button.config(state=tk.DISABLED)
|
||||||
@@ -246,13 +238,12 @@ class WidgetManager:
|
|||||||
command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3)
|
command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3)
|
||||||
|
|
||||||
def _setup_sidebar(self, parent_paned_window: ttk.PanedWindow) -> None:
|
def _setup_sidebar(self, parent_paned_window: ttk.PanedWindow) -> None:
|
||||||
"""Sets up the sidebar with bookmarks and devices."""
|
|
||||||
sidebar_frame = ttk.Frame(
|
sidebar_frame = ttk.Frame(
|
||||||
parent_paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200)
|
parent_paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200)
|
||||||
sidebar_frame.grid_propagate(False)
|
sidebar_frame.grid_propagate(False)
|
||||||
sidebar_frame.bind("<Configure>", self.dialog.on_sidebar_resize)
|
sidebar_frame.bind("<Configure>", self.dialog.on_sidebar_resize)
|
||||||
parent_paned_window.add(sidebar_frame, weight=0)
|
parent_paned_window.add(sidebar_frame, weight=0)
|
||||||
sidebar_frame.grid_rowconfigure(2, weight=1)
|
sidebar_frame.grid_rowconfigure(4, weight=1)
|
||||||
|
|
||||||
self._setup_sidebar_bookmarks(sidebar_frame)
|
self._setup_sidebar_bookmarks(sidebar_frame)
|
||||||
|
|
||||||
@@ -265,10 +256,14 @@ class WidgetManager:
|
|||||||
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
|
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
|
||||||
row=3, column=0, sticky="ew", padx=20, pady=15)
|
row=3, column=0, sticky="ew", padx=20, pady=15)
|
||||||
|
|
||||||
|
self._setup_sidebar_sftp_bookmarks(sidebar_frame)
|
||||||
|
|
||||||
|
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
|
||||||
|
row=5, column=0, sticky="ew", padx=20, pady=15)
|
||||||
|
|
||||||
self._setup_sidebar_storage(sidebar_frame)
|
self._setup_sidebar_storage(sidebar_frame)
|
||||||
|
|
||||||
def _setup_sidebar_bookmarks(self, sidebar_frame: ttk.Frame) -> None:
|
def _setup_sidebar_bookmarks(self, sidebar_frame: ttk.Frame) -> None:
|
||||||
"""Sets up the bookmark buttons in the sidebar."""
|
|
||||||
sidebar_buttons_frame = ttk.Frame(
|
sidebar_buttons_frame = ttk.Frame(
|
||||||
sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0))
|
sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0))
|
||||||
sidebar_buttons_frame.grid(row=0, column=0, sticky="nsew")
|
sidebar_buttons_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
@@ -293,8 +288,47 @@ class WidgetManager:
|
|||||||
btn.pack(fill="x", pady=1)
|
btn.pack(fill="x", pady=1)
|
||||||
self.sidebar_buttons.append((btn, f" {config['name']}"))
|
self.sidebar_buttons.append((btn, f" {config['name']}"))
|
||||||
|
|
||||||
|
def _setup_sidebar_sftp_bookmarks(self, sidebar_frame: ttk.Frame) -> None:
|
||||||
|
self.sftp_bookmarks_frame = ttk.Frame(
|
||||||
|
sidebar_frame, style="Sidebar.TFrame")
|
||||||
|
self.sftp_bookmarks_frame.grid(row=4, column=0, sticky="nsew", padx=10)
|
||||||
|
self.sftp_bookmarks_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
bookmarks = self.dialog.config_manager.load_bookmarks()
|
||||||
|
if not bookmarks:
|
||||||
|
return
|
||||||
|
|
||||||
|
ttk.Label(self.sftp_bookmarks_frame, text=LocaleStrings.UI["sftp_bookmarks"], background=self.style_manager.sidebar_color,
|
||||||
|
foreground=self.style_manager.color_foreground).grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 0))
|
||||||
|
|
||||||
|
self.sftp_bookmark_buttons = []
|
||||||
|
sftp_bookmark_icon = self.dialog.icon_manager.get_icon('star')
|
||||||
|
|
||||||
|
row_counter = 1
|
||||||
|
for name, data in bookmarks.items():
|
||||||
|
if sftp_bookmark_icon:
|
||||||
|
btn = ttk.Button(self.sftp_bookmarks_frame, text=f" {name}", image=sftp_bookmark_icon, compound="left",
|
||||||
|
command=lambda d=data: self.dialog.connect_sftp_bookmark(d), style="Dark.TButton.Borderless")
|
||||||
|
else:
|
||||||
|
btn = ttk.Button(self.sftp_bookmarks_frame, text=f" {name}", compound="left",
|
||||||
|
command=lambda d=data: self.dialog.connect_sftp_bookmark(d), style="Dark.TButton.Borderless")
|
||||||
|
btn.grid(row=row_counter, column=0, sticky="ew")
|
||||||
|
row_counter += 1
|
||||||
|
btn.bind("<Button-3>", lambda event,
|
||||||
|
n=name: self._show_sftp_bookmark_context_menu(event, n))
|
||||||
|
self.sftp_bookmark_buttons.append(btn)
|
||||||
|
|
||||||
|
def _show_sftp_bookmark_context_menu(self, event, name):
|
||||||
|
context_menu = tk.Menu(self.dialog, tearoff=0)
|
||||||
|
trash_icon = self.dialog.icon_manager.get_icon('trash_small2')
|
||||||
|
context_menu.add_command(
|
||||||
|
label=LocaleStrings.UI["remove_bookmark"],
|
||||||
|
image=trash_icon,
|
||||||
|
compound=tk.LEFT,
|
||||||
|
command=lambda: self.dialog.remove_sftp_bookmark(name))
|
||||||
|
context_menu.tk_popup(event.x_root, event.y_root)
|
||||||
|
|
||||||
def _setup_sidebar_devices(self, sidebar_frame: ttk.Frame) -> None:
|
def _setup_sidebar_devices(self, sidebar_frame: ttk.Frame) -> None:
|
||||||
"""Sets up the mounted devices section in the sidebar."""
|
|
||||||
mounted_devices_frame = ttk.Frame(
|
mounted_devices_frame = ttk.Frame(
|
||||||
sidebar_frame, style="Sidebar.TFrame")
|
sidebar_frame, style="Sidebar.TFrame")
|
||||||
mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10)
|
mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10)
|
||||||
@@ -316,13 +350,6 @@ class WidgetManager:
|
|||||||
self.devices_canvas_window = self.devices_canvas.create_window(
|
self.devices_canvas_window = self.devices_canvas.create_window(
|
||||||
(0, 0), window=self.devices_scrollable_frame, anchor="nw")
|
(0, 0), window=self.devices_scrollable_frame, anchor="nw")
|
||||||
|
|
||||||
self.devices_canvas.bind("<Enter>", self.dialog._on_devices_enter)
|
|
||||||
self.devices_canvas.bind("<Leave>", self.dialog._on_devices_leave)
|
|
||||||
self.devices_scrollable_frame.bind(
|
|
||||||
"<Enter>", self.dialog._on_devices_enter)
|
|
||||||
self.devices_scrollable_frame.bind(
|
|
||||||
"<Leave>", self.dialog._on_devices_leave)
|
|
||||||
|
|
||||||
def _configure_devices_canvas(event: tk.Event) -> None:
|
def _configure_devices_canvas(event: tk.Event) -> None:
|
||||||
self.devices_canvas.configure(
|
self.devices_canvas.configure(
|
||||||
scrollregion=self.devices_canvas.bbox("all"))
|
scrollregion=self.devices_canvas.bbox("all"))
|
||||||
@@ -365,8 +392,6 @@ class WidgetManager:
|
|||||||
w.bind("<MouseWheel>", _on_devices_mouse_wheel)
|
w.bind("<MouseWheel>", _on_devices_mouse_wheel)
|
||||||
w.bind("<Button-4>", _on_devices_mouse_wheel)
|
w.bind("<Button-4>", _on_devices_mouse_wheel)
|
||||||
w.bind("<Button-5>", _on_devices_mouse_wheel)
|
w.bind("<Button-5>", _on_devices_mouse_wheel)
|
||||||
w.bind("<Enter>", self.dialog._on_devices_enter)
|
|
||||||
w.bind("<Leave>", self.dialog._on_devices_leave)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
total, used, _ = shutil.disk_usage(mount_point)
|
total, used, _ = shutil.disk_usage(mount_point)
|
||||||
@@ -378,24 +403,20 @@ class WidgetManager:
|
|||||||
w.bind("<MouseWheel>", _on_devices_mouse_wheel)
|
w.bind("<MouseWheel>", _on_devices_mouse_wheel)
|
||||||
w.bind("<Button-4>", _on_devices_mouse_wheel)
|
w.bind("<Button-4>", _on_devices_mouse_wheel)
|
||||||
w.bind("<Button-5>", _on_devices_mouse_wheel)
|
w.bind("<Button-5>", _on_devices_mouse_wheel)
|
||||||
w.bind("<Enter>", self.dialog._on_devices_enter)
|
|
||||||
w.bind("<Leave>", self.dialog._on_devices_leave)
|
|
||||||
except (FileNotFoundError, PermissionError):
|
except (FileNotFoundError, PermissionError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _setup_sidebar_storage(self, sidebar_frame: ttk.Frame) -> None:
|
def _setup_sidebar_storage(self, sidebar_frame: ttk.Frame) -> None:
|
||||||
"""Sets up the storage indicator in the sidebar."""
|
|
||||||
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
|
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
|
||||||
storage_frame.grid(row=5, column=0, sticky="sew", padx=10, pady=10)
|
storage_frame.grid(row=6, column=0, sticky="sew", padx=10, pady=10)
|
||||||
self.storage_label = ttk.Label(
|
self.storage_label = ttk.Label(
|
||||||
storage_frame, text=f"{LocaleStrings.CFD["free_space"]}:", background=self.style_manager.freespace_background)
|
storage_frame, text=f"{LocaleStrings.CFD['free_space']}", background=self.style_manager.freespace_background)
|
||||||
self.storage_label.pack(fill="x", padx=10)
|
self.storage_label.pack(fill="x", padx=10)
|
||||||
self.storage_bar = ttk.Progressbar(
|
self.storage_bar = ttk.Progressbar(
|
||||||
storage_frame, orient="horizontal", length=100, mode="determinate")
|
storage_frame, orient="horizontal", length=100, mode="determinate")
|
||||||
self.storage_bar.pack(fill="x", pady=(2, 5), padx=15)
|
self.storage_bar.pack(fill="x", pady=(2, 5), padx=15)
|
||||||
|
|
||||||
def _setup_bottom_bar(self) -> None:
|
def _setup_bottom_bar(self) -> None:
|
||||||
"""Sets up the bottom bar including filename entry, action buttons, and status info."""
|
|
||||||
self.action_status_frame = ttk.Frame(
|
self.action_status_frame = ttk.Frame(
|
||||||
self.content_frame, style="AccentBottom.TFrame")
|
self.content_frame, style="AccentBottom.TFrame")
|
||||||
self.action_status_frame.grid(
|
self.action_status_frame.grid(
|
||||||
@@ -423,7 +444,6 @@ class WidgetManager:
|
|||||||
sticky="ew", pady=(4, 0))
|
sticky="ew", pady=(4, 0))
|
||||||
self.status_container.grid(row=2, column=0, columnspan=3, sticky='ew')
|
self.status_container.grid(row=2, column=0, columnspan=3, sticky='ew')
|
||||||
|
|
||||||
# --- Define Widgets ---
|
|
||||||
self.search_status_label = ttk.Label(
|
self.search_status_label = ttk.Label(
|
||||||
self.status_container, text="", style="AccentBottom.TLabel")
|
self.status_container, text="", style="AccentBottom.TLabel")
|
||||||
self.filename_entry = ttk.Entry(self.center_container)
|
self.filename_entry = ttk.Entry(self.center_container)
|
||||||
@@ -450,7 +470,7 @@ class WidgetManager:
|
|||||||
self.cancel_button = ttk.Button(
|
self.cancel_button = ttk.Button(
|
||||||
self.action_status_frame, text=LocaleStrings.SET["cancel_button"], command=self.dialog.on_cancel)
|
self.action_status_frame, text=LocaleStrings.SET["cancel_button"], command=self.dialog.on_cancel)
|
||||||
self.filter_combobox = ttk.Combobox(self.center_container, values=[
|
self.filter_combobox = ttk.Combobox(self.center_container, values=[
|
||||||
ft[0] for ft in self.dialog.filetypes], state="readonly")
|
ft[0] for ft in self.dialog.filetypes], state="readonly")
|
||||||
self.filter_combobox.bind(
|
self.filter_combobox.bind(
|
||||||
"<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
"<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
||||||
self.filter_combobox.set(self.dialog.filetypes[0][0])
|
self.filter_combobox.set(self.dialog.filetypes[0][0])
|
||||||
@@ -466,7 +486,7 @@ class WidgetManager:
|
|||||||
self.cancel_button = ttk.Button(
|
self.cancel_button = ttk.Button(
|
||||||
self.action_status_frame, text=LocaleStrings.CFD["cancel"], command=self.dialog.on_cancel)
|
self.action_status_frame, text=LocaleStrings.CFD["cancel"], command=self.dialog.on_cancel)
|
||||||
self.filter_combobox = ttk.Combobox(self.center_container, values=[
|
self.filter_combobox = ttk.Combobox(self.center_container, values=[
|
||||||
ft[0] for ft in self.dialog.filetypes], state="readonly")
|
ft[0] for ft in self.dialog.filetypes], state="readonly")
|
||||||
self.filter_combobox.bind(
|
self.filter_combobox.bind(
|
||||||
"<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
"<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
||||||
self.filter_combobox.set(self.dialog.filetypes[0][0])
|
self.filter_combobox.set(self.dialog.filetypes[0][0])
|
||||||
@@ -478,21 +498,16 @@ class WidgetManager:
|
|||||||
self._layout_bottom_buttons(button_box_pos)
|
self._layout_bottom_buttons(button_box_pos)
|
||||||
|
|
||||||
def _layout_bottom_buttons(self, button_box_pos: str) -> None:
|
def _layout_bottom_buttons(self, button_box_pos: str) -> None:
|
||||||
"""Lays out the bottom action buttons based on user settings."""
|
|
||||||
# Configure container weights
|
|
||||||
self.left_container.grid_rowconfigure(0, weight=1)
|
self.left_container.grid_rowconfigure(0, weight=1)
|
||||||
self.right_container.grid_rowconfigure(0, weight=1)
|
self.right_container.grid_rowconfigure(0, weight=1)
|
||||||
|
|
||||||
# Determine action button and its container
|
|
||||||
action_button = self.save_button if self.dialog.dialog_mode == "save" else self.open_button
|
action_button = self.save_button if self.dialog.dialog_mode == "save" else self.open_button
|
||||||
action_container = self.left_container if button_box_pos == 'left' else self.right_container
|
action_container = self.left_container if button_box_pos == 'left' else self.right_container
|
||||||
other_container = self.right_container if button_box_pos == 'left' else self.left_container
|
other_container = self.right_container if button_box_pos == 'left' else self.left_container
|
||||||
|
|
||||||
# Place main action buttons
|
|
||||||
action_button.grid(in_=action_container, row=0, column=0, pady=(0, 5))
|
action_button.grid(in_=action_container, row=0, column=0, pady=(0, 5))
|
||||||
self.cancel_button.grid(in_=action_container, row=1, column=0)
|
self.cancel_button.grid(in_=action_container, row=1, column=0)
|
||||||
|
|
||||||
# Place settings and trash buttons
|
|
||||||
if button_box_pos == 'left':
|
if button_box_pos == 'left':
|
||||||
self.settings_button.grid(
|
self.settings_button.grid(
|
||||||
in_=other_container, row=0, column=0, sticky="ne")
|
in_=other_container, row=0, column=0, sticky="ne")
|
||||||
@@ -506,7 +521,6 @@ class WidgetManager:
|
|||||||
self.trash_button.grid(
|
self.trash_button.grid(
|
||||||
in_=other_container, row=0, column=0, sticky="sw")
|
in_=other_container, row=0, column=0, sticky="sw")
|
||||||
|
|
||||||
# Layout for the center container (filename, filter, status)
|
|
||||||
if button_box_pos == 'left':
|
if button_box_pos == 'left':
|
||||||
self.center_container.grid_columnconfigure(0, weight=1)
|
self.center_container.grid_columnconfigure(0, weight=1)
|
||||||
self.filter_combobox.grid(
|
self.filter_combobox.grid(
|
||||||
@@ -517,8 +531,6 @@ class WidgetManager:
|
|||||||
in_=self.center_container, row=1, column=1, sticky="e", pady=(5, 0))
|
in_=self.center_container, row=1, column=1, sticky="e", pady=(5, 0))
|
||||||
|
|
||||||
def setup_widgets(self) -> None:
|
def setup_widgets(self) -> None:
|
||||||
"""Creates and arranges all widgets in the main dialog window."""
|
|
||||||
# Main container
|
|
||||||
main_frame = ttk.Frame(self.dialog, style='Accent.TFrame')
|
main_frame = ttk.Frame(self.dialog, style='Accent.TFrame')
|
||||||
main_frame.pack(fill="both", expand=True)
|
main_frame.pack(fill="both", expand=True)
|
||||||
main_frame.grid_rowconfigure(2, weight=1)
|
main_frame.grid_rowconfigure(2, weight=1)
|
||||||
@@ -526,12 +538,10 @@ class WidgetManager:
|
|||||||
|
|
||||||
self._setup_top_bar(main_frame)
|
self._setup_top_bar(main_frame)
|
||||||
|
|
||||||
# Horizontal separator
|
|
||||||
separator_color = "#000000" if self.style_manager.is_dark else "#9c9c9c"
|
separator_color = "#000000" if self.style_manager.is_dark else "#9c9c9c"
|
||||||
tk.Frame(main_frame, height=1, bg=separator_color).grid(
|
tk.Frame(main_frame, height=1, bg=separator_color).grid(
|
||||||
row=1, column=0, columnspan=2, sticky="ew")
|
row=1, column=0, columnspan=2, sticky="ew")
|
||||||
|
|
||||||
# PanedWindow for resizable sidebar and content
|
|
||||||
paned_window = ttk.PanedWindow(
|
paned_window = ttk.PanedWindow(
|
||||||
main_frame, orient=tk.HORIZONTAL, style="Sidebar.TFrame")
|
main_frame, orient=tk.HORIZONTAL, style="Sidebar.TFrame")
|
||||||
paned_window.grid(row=2, column=0, columnspan=2, sticky="nsew")
|
paned_window.grid(row=2, column=0, columnspan=2, sticky="nsew")
|
||||||
@@ -549,5 +559,4 @@ class WidgetManager:
|
|||||||
self.file_list_frame.grid(row=0, column=0, sticky="nsew")
|
self.file_list_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
self.dialog.bind("<Configure>", self.dialog.on_window_resize)
|
self.dialog.bind("<Configure>", self.dialog.on_window_resize)
|
||||||
|
|
||||||
# --- Bottom Bar ---
|
|
||||||
self._setup_bottom_bar()
|
self._setup_bottom_bar()
|
||||||
|
@@ -2,7 +2,7 @@ import os
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List, Tuple, Callable, Any
|
from typing import Optional, List, Tuple, Callable, Any, Dict
|
||||||
|
|
||||||
# To avoid circular import with custom_file_dialog.py
|
# To avoid circular import with custom_file_dialog.py
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -10,6 +10,7 @@ if TYPE_CHECKING:
|
|||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
|
|
||||||
from shared_libs.common_tools import Tooltip
|
from shared_libs.common_tools import Tooltip
|
||||||
|
from shared_libs.message import MessageDialog
|
||||||
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
||||||
|
|
||||||
|
|
||||||
@@ -25,18 +26,66 @@ class ViewManager:
|
|||||||
"""
|
"""
|
||||||
self.dialog = dialog
|
self.dialog = dialog
|
||||||
|
|
||||||
|
def _get_file_info_list(self) -> Tuple[List[Dict], Optional[str], Optional[str]]:
|
||||||
|
"""
|
||||||
|
Gets a sorted list of file information dictionaries from the current source.
|
||||||
|
"""
|
||||||
|
if self.dialog.current_fs_type == "sftp":
|
||||||
|
items, error = self.dialog.sftp_manager.list_directory(self.dialog.current_dir)
|
||||||
|
if error:
|
||||||
|
return [], error, None
|
||||||
|
|
||||||
|
file_info_list = []
|
||||||
|
import stat
|
||||||
|
for item in items:
|
||||||
|
if item.filename in ['.', '..']:
|
||||||
|
continue
|
||||||
|
is_dir = stat.S_ISDIR(item.st_mode)
|
||||||
|
# Manually construct SFTP path to ensure forward slashes
|
||||||
|
path = f"{self.dialog.current_dir}/{item.filename}".replace("//", "/")
|
||||||
|
file_info_list.append({
|
||||||
|
'name': item.filename,
|
||||||
|
'path': path,
|
||||||
|
'is_dir': is_dir,
|
||||||
|
'size': item.st_size,
|
||||||
|
'modified': item.st_mtime
|
||||||
|
})
|
||||||
|
return file_info_list, None, None
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
items = list(os.scandir(self.dialog.current_dir))
|
||||||
|
num_items = len(items)
|
||||||
|
warning_message = None
|
||||||
|
if num_items > CfdConfigManager.MAX_ITEMS_TO_DISPLAY:
|
||||||
|
warning_message = f"{LocaleStrings.CFD['showing']} {CfdConfigManager.MAX_ITEMS_TO_DISPLAY} {LocaleStrings.CFD['of']} {num_items} {LocaleStrings.CFD['entries']}."
|
||||||
|
items = items[:CfdConfigManager.MAX_ITEMS_TO_DISPLAY]
|
||||||
|
|
||||||
|
items.sort(key=lambda e: (not e.is_dir(), e.name.lower()))
|
||||||
|
|
||||||
|
file_info_list = []
|
||||||
|
for item in items:
|
||||||
|
try:
|
||||||
|
stat_result = item.stat()
|
||||||
|
file_info_list.append({
|
||||||
|
'name': item.name,
|
||||||
|
'path': item.path,
|
||||||
|
'is_dir': item.is_dir(),
|
||||||
|
'size': stat_result.st_size,
|
||||||
|
'modified': stat_result.st_mtime
|
||||||
|
})
|
||||||
|
except (FileNotFoundError, PermissionError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return file_info_list, None, warning_message
|
||||||
|
except PermissionError:
|
||||||
|
return ([], LocaleStrings.CFD["access_denied"], None)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return ([], LocaleStrings.CFD["directory_not_found"], None)
|
||||||
|
|
||||||
def populate_files(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
def populate_files(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Populates the main file display area.
|
Populates the main file display area.
|
||||||
|
|
||||||
This method clears the current view and then calls the appropriate
|
|
||||||
method to populate either the list or icon view.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item_to_rename (str, optional): The name of an item to immediately
|
|
||||||
put into rename mode. Defaults to None.
|
|
||||||
item_to_select (str, optional): The name of an item to select
|
|
||||||
after populating. Defaults to None.
|
|
||||||
"""
|
"""
|
||||||
self._unbind_mouse_wheel_events()
|
self._unbind_mouse_wheel_events()
|
||||||
|
|
||||||
@@ -46,46 +95,19 @@ class ViewManager:
|
|||||||
self.dialog.widget_manager.path_entry.insert(
|
self.dialog.widget_manager.path_entry.insert(
|
||||||
0, self.dialog.current_dir)
|
0, self.dialog.current_dir)
|
||||||
self.dialog.result = None
|
self.dialog.result = None
|
||||||
self.dialog.update_status_bar()
|
self.dialog.selected_item_frames.clear()
|
||||||
|
self.dialog.update_selection_info()
|
||||||
if self.dialog.view_mode.get() == "list":
|
if self.dialog.view_mode.get() == "list":
|
||||||
self.populate_list_view(item_to_rename, item_to_select)
|
self.populate_list_view(item_to_rename, item_to_select)
|
||||||
else:
|
else:
|
||||||
self.populate_icon_view(item_to_rename, item_to_select)
|
self.populate_icon_view(item_to_rename, item_to_select)
|
||||||
|
|
||||||
def _get_sorted_items(self) -> Tuple[List[str], Optional[str], Optional[str]]:
|
|
||||||
"""
|
|
||||||
Gets a sorted list of items from the current directory.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: A tuple containing (list of items, error message, warning message).
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
items = os.listdir(self.dialog.current_dir)
|
|
||||||
num_items = len(items)
|
|
||||||
warning_message = None
|
|
||||||
if num_items > CfdConfigManager.MAX_ITEMS_TO_DISPLAY:
|
|
||||||
warning_message = f"{LocaleStrings.CFD['showing']} {CfdConfigManager.MAX_ITEMS_TO_DISPLAY} {LocaleStrings.CFD['of']} {num_items} {LocaleStrings.CFD['entries']}."
|
|
||||||
items = items[:CfdConfigManager.MAX_ITEMS_TO_DISPLAY]
|
|
||||||
dirs = sorted([d for d in items if os.path.isdir(
|
|
||||||
os.path.join(self.dialog.current_dir, d))], key=str.lower)
|
|
||||||
files = sorted([f for f in items if not os.path.isdir(
|
|
||||||
os.path.join(self.dialog.current_dir, f))], key=str.lower)
|
|
||||||
return (dirs + files, None, warning_message)
|
|
||||||
except PermissionError:
|
|
||||||
return ([], LocaleStrings.CFD["access_denied"], None)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return ([], LocaleStrings.CFD["directory_not_found"], None)
|
|
||||||
|
|
||||||
def _get_folder_content_count(self, folder_path: str) -> Optional[int]:
|
def _get_folder_content_count(self, folder_path: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Counts the number of items in a given folder.
|
Counts the number of items in a given folder.
|
||||||
|
|
||||||
Args:
|
|
||||||
folder_path (str): The path to the folder.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int or None: The number of items, or None if an error occurs.
|
|
||||||
"""
|
"""
|
||||||
|
if self.dialog.current_fs_type == "sftp":
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
if not os.path.isdir(folder_path) or not os.access(folder_path, os.R_OK):
|
if not os.path.isdir(folder_path) or not os.access(folder_path, os.R_OK):
|
||||||
return None
|
return None
|
||||||
@@ -102,17 +124,21 @@ class ViewManager:
|
|||||||
def _get_item_path_from_widget(self, widget: tk.Widget) -> Optional[str]:
|
def _get_item_path_from_widget(self, widget: tk.Widget) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Traverses up the widget hierarchy to find the item_path attribute.
|
Traverses up the widget hierarchy to find the item_path attribute.
|
||||||
|
|
||||||
Args:
|
|
||||||
widget: The widget to start from.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str or None: The associated file path, or None if not found.
|
|
||||||
"""
|
"""
|
||||||
while widget and not hasattr(widget, 'item_path'):
|
while widget and not hasattr(widget, 'item_path'):
|
||||||
widget = widget.master
|
widget = widget.master
|
||||||
return getattr(widget, 'item_path', None)
|
return getattr(widget, 'item_path', None)
|
||||||
|
|
||||||
|
def _is_dir(self, path: str) -> bool:
|
||||||
|
"""Checks if a given path is a directory, supporting both local and SFTP."""
|
||||||
|
if self.dialog.current_fs_type == 'sftp':
|
||||||
|
for item in self.dialog.all_items:
|
||||||
|
if item['path'] == path:
|
||||||
|
return item['is_dir']
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return os.path.isdir(path)
|
||||||
|
|
||||||
def _handle_icon_click(self, event: tk.Event) -> None:
|
def _handle_icon_click(self, event: tk.Event) -> None:
|
||||||
"""Handles a single click on an icon view item."""
|
"""Handles a single click on an icon view item."""
|
||||||
item_path = self._get_item_path_from_widget(event.widget)
|
item_path = self._get_item_path_from_widget(event.widget)
|
||||||
@@ -120,7 +146,7 @@ class ViewManager:
|
|||||||
item_frame = event.widget
|
item_frame = event.widget
|
||||||
while not hasattr(item_frame, 'item_path'):
|
while not hasattr(item_frame, 'item_path'):
|
||||||
item_frame = item_frame.master
|
item_frame = item_frame.master
|
||||||
self.on_item_select(item_path, item_frame)
|
self.on_item_select(item_path, item_frame, event)
|
||||||
|
|
||||||
def _handle_icon_double_click(self, event: tk.Event) -> None:
|
def _handle_icon_double_click(self, event: tk.Event) -> None:
|
||||||
"""Handles a double click on an icon view item."""
|
"""Handles a double click on an icon view item."""
|
||||||
@@ -147,12 +173,8 @@ class ViewManager:
|
|||||||
def populate_icon_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
def populate_icon_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Populates the file display with items in an icon grid layout.
|
Populates the file display with items in an icon grid layout.
|
||||||
|
|
||||||
Args:
|
|
||||||
item_to_rename (str, optional): Item to enter rename mode.
|
|
||||||
item_to_select (str, optional): Item to select.
|
|
||||||
"""
|
"""
|
||||||
self.dialog.all_items, error, warning = self._get_sorted_items()
|
self.dialog.all_items, error, warning = self._get_file_info_list()
|
||||||
self.dialog.currently_loaded_count = 0
|
self.dialog.currently_loaded_count = 0
|
||||||
|
|
||||||
self.dialog.icon_canvas = tk.Canvas(self.dialog.widget_manager.file_list_frame,
|
self.dialog.icon_canvas = tk.Canvas(self.dialog.widget_manager.file_list_frame,
|
||||||
@@ -223,15 +245,6 @@ class ViewManager:
|
|||||||
def _load_more_items_icon_view(self, container: ttk.Frame, scroll_handler: Callable, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> Optional[ttk.Frame]:
|
def _load_more_items_icon_view(self, container: ttk.Frame, scroll_handler: Callable, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> Optional[ttk.Frame]:
|
||||||
"""
|
"""
|
||||||
Loads a batch of items into the icon view.
|
Loads a batch of items into the icon view.
|
||||||
|
|
||||||
Args:
|
|
||||||
container: The parent widget for the items.
|
|
||||||
scroll_handler: The function to handle mouse wheel events.
|
|
||||||
item_to_rename (str, optional): Item to enter rename mode.
|
|
||||||
item_to_select (str, optional): Item to select.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The widget that was focused (renamed or selected), or None.
|
|
||||||
"""
|
"""
|
||||||
start_index = self.dialog.currently_loaded_count
|
start_index = self.dialog.currently_loaded_count
|
||||||
end_index = min(len(self.dialog.all_items), start_index +
|
end_index = min(len(self.dialog.all_items), start_index +
|
||||||
@@ -250,11 +263,13 @@ class ViewManager:
|
|||||||
widget_to_focus = None
|
widget_to_focus = None
|
||||||
|
|
||||||
for i in range(start_index, end_index):
|
for i in range(start_index, end_index):
|
||||||
name = self.dialog.all_items[i]
|
file_info = self.dialog.all_items[i]
|
||||||
|
name = file_info['name']
|
||||||
|
path = file_info['path']
|
||||||
|
is_dir = file_info['is_dir']
|
||||||
|
|
||||||
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
||||||
continue
|
continue
|
||||||
path = os.path.join(self.dialog.current_dir, name)
|
|
||||||
is_dir = os.path.isdir(path)
|
|
||||||
if not is_dir and not self.dialog._matches_filetype(name):
|
if not is_dir and not self.dialog._matches_filetype(name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -283,7 +298,7 @@ class ViewManager:
|
|||||||
widget.bind("<Double-Button-1>", lambda e,
|
widget.bind("<Double-Button-1>", lambda e,
|
||||||
p=path: self._handle_item_double_click(p))
|
p=path: self._handle_item_double_click(p))
|
||||||
widget.bind("<Button-1>", lambda e, p=path,
|
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,
|
widget.bind("<ButtonRelease-3>", lambda e,
|
||||||
p=path: self.dialog.file_op_manager._show_context_menu(e, p))
|
p=path: self.dialog.file_op_manager._show_context_menu(e, p))
|
||||||
widget.bind("<F2>", lambda e, p=path,
|
widget.bind("<F2>", lambda e, p=path,
|
||||||
@@ -309,12 +324,8 @@ class ViewManager:
|
|||||||
def populate_list_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
def populate_list_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Populates the file display with items in a list (Treeview) layout.
|
Populates the file display with items in a list (Treeview) layout.
|
||||||
|
|
||||||
Args:
|
|
||||||
item_to_rename (str, optional): Item to enter rename mode.
|
|
||||||
item_to_select (str, optional): Item to select.
|
|
||||||
"""
|
"""
|
||||||
self.dialog.all_items, error, warning = self._get_sorted_items()
|
self.dialog.all_items, error, warning = self._get_file_info_list()
|
||||||
self.dialog.currently_loaded_count = 0
|
self.dialog.currently_loaded_count = 0
|
||||||
|
|
||||||
tree_frame = ttk.Frame(self.dialog.widget_manager.file_list_frame)
|
tree_frame = ttk.Frame(self.dialog.widget_manager.file_list_frame)
|
||||||
@@ -325,6 +336,9 @@ class ViewManager:
|
|||||||
columns = ("size", "type", "modified")
|
columns = ("size", "type", "modified")
|
||||||
self.dialog.tree = ttk.Treeview(
|
self.dialog.tree = ttk.Treeview(
|
||||||
tree_frame, columns=columns, show="tree headings")
|
tree_frame, columns=columns, show="tree headings")
|
||||||
|
|
||||||
|
if self.dialog.dialog_mode == 'multi':
|
||||||
|
self.dialog.tree.config(selectmode="extended")
|
||||||
|
|
||||||
self.dialog.tree.heading(
|
self.dialog.tree.heading(
|
||||||
"#0", text=LocaleStrings.VIEW["name"], anchor="w")
|
"#0", text=LocaleStrings.VIEW["name"], anchor="w")
|
||||||
@@ -381,13 +395,6 @@ class ViewManager:
|
|||||||
def _load_more_items_list_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> bool:
|
def _load_more_items_list_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Loads a batch of items into the list view.
|
Loads a batch of items into the list view.
|
||||||
|
|
||||||
Args:
|
|
||||||
item_to_rename (str, optional): Item to enter rename mode.
|
|
||||||
item_to_select (str, optional): Item to select.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the item to rename/select was found and processed.
|
|
||||||
"""
|
"""
|
||||||
start_index = self.dialog.currently_loaded_count
|
start_index = self.dialog.currently_loaded_count
|
||||||
end_index = min(len(self.dialog.all_items), start_index +
|
end_index = min(len(self.dialog.all_items), start_index +
|
||||||
@@ -398,25 +405,28 @@ class ViewManager:
|
|||||||
|
|
||||||
item_found = False
|
item_found = False
|
||||||
for i in range(start_index, end_index):
|
for i in range(start_index, end_index):
|
||||||
name = self.dialog.all_items[i]
|
file_info = self.dialog.all_items[i]
|
||||||
|
name = file_info['name']
|
||||||
|
path = file_info['path']
|
||||||
|
is_dir = file_info['is_dir']
|
||||||
|
|
||||||
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
||||||
continue
|
continue
|
||||||
path = os.path.join(self.dialog.current_dir, name)
|
|
||||||
is_dir = os.path.isdir(path)
|
|
||||||
if not is_dir and not self.dialog._matches_filetype(name):
|
if not is_dir and not self.dialog._matches_filetype(name):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
stat = os.stat(path)
|
|
||||||
modified_time = datetime.fromtimestamp(
|
modified_time = datetime.fromtimestamp(
|
||||||
stat.st_mtime).strftime('%d.%m.%Y %H:%M')
|
file_info['modified']).strftime('%d.%m.%Y %H:%M')
|
||||||
if is_dir:
|
if is_dir:
|
||||||
icon, file_type, size = self.dialog.icon_manager.get_icon(
|
icon, file_type, size = self.dialog.icon_manager.get_icon(
|
||||||
'folder_small'), LocaleStrings.FILE["folder"], ""
|
'folder_small'), LocaleStrings.FILE["folder"], ""
|
||||||
else:
|
else:
|
||||||
icon, file_type, size = self.dialog.get_file_icon(
|
icon, file_type, size = self.dialog.get_file_icon(
|
||||||
name, 'small'), LocaleStrings.FILE["file"], self.dialog._format_size(stat.st_size)
|
name, 'small'), LocaleStrings.FILE["file"], self.dialog._format_size(file_info['size'])
|
||||||
item_id = self.dialog.tree.insert("", "end", text=f" {name}", image=icon, values=(
|
item_id = self.dialog.tree.insert("", "end", text=f" {name}", image=icon, values=(
|
||||||
size, file_type, modified_time))
|
size, file_type, modified_time))
|
||||||
|
self.dialog.item_path_map[item_id] = path # Store path for later retrieval
|
||||||
|
|
||||||
if name == item_to_rename:
|
if name == item_to_rename:
|
||||||
self.dialog.tree.selection_set(item_id)
|
self.dialog.tree.selection_set(item_id)
|
||||||
self.dialog.tree.focus(item_id)
|
self.dialog.tree.focus(item_id)
|
||||||
@@ -434,70 +444,158 @@ class ViewManager:
|
|||||||
self.dialog.currently_loaded_count = end_index
|
self.dialog.currently_loaded_count = end_index
|
||||||
return item_found
|
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.
|
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.
|
|
||||||
"""
|
"""
|
||||||
if hasattr(self.dialog, 'selected_item_frame') and self.dialog.selected_item_frame.winfo_exists():
|
if self.dialog.dialog_mode == 'dir' and not self._is_dir(path):
|
||||||
self.dialog.selected_item_frame.state(['!selected'])
|
return
|
||||||
for child in self.dialog.selected_item_frame.winfo_children():
|
|
||||||
|
if self.dialog.dialog_mode == 'multi':
|
||||||
|
ctrl_pressed = (event.state & 0x4) != 0 if event else False
|
||||||
|
|
||||||
|
if ctrl_pressed:
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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'])
|
||||||
|
for child in self.dialog.selected_item_frame.winfo_children():
|
||||||
|
if isinstance(child, ttk.Label):
|
||||||
|
child.state(['!selected'])
|
||||||
|
item_frame.state(['selected'])
|
||||||
|
for child in item_frame.winfo_children():
|
||||||
if isinstance(child, ttk.Label):
|
if isinstance(child, ttk.Label):
|
||||||
child.state(['!selected'])
|
child.state(['selected'])
|
||||||
item_frame.state(['selected'])
|
self.dialog.selected_item_frame = item_frame
|
||||||
for child in item_frame.winfo_children():
|
self.dialog.update_selection_info(path)
|
||||||
if isinstance(child, ttk.Label):
|
|
||||||
child.state(['selected'])
|
|
||||||
self.dialog.selected_item_frame = item_frame
|
|
||||||
self.dialog.selected_file = path
|
|
||||||
self.dialog.update_status_bar(path)
|
|
||||||
self.dialog.search_manager.show_search_ready()
|
self.dialog.search_manager.show_search_ready()
|
||||||
if not os.path.isdir(path):
|
|
||||||
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:
|
def on_list_select(self, event: tk.Event) -> None:
|
||||||
"""Handles the selection of an item in the list view."""
|
"""
|
||||||
if not self.dialog.tree.selection():
|
Handles the selection of an item in the list view.
|
||||||
|
"""
|
||||||
|
selections = self.dialog.tree.selection()
|
||||||
|
if not selections:
|
||||||
|
self.dialog.result = [] if self.dialog.dialog_mode == 'multi' else None
|
||||||
|
self.dialog.update_selection_info()
|
||||||
return
|
return
|
||||||
item_id = self.dialog.tree.selection()[0]
|
|
||||||
item_text = self.dialog.tree.item(item_id, 'text').strip()
|
if self.dialog.dialog_mode == 'multi':
|
||||||
path = os.path.join(self.dialog.current_dir, item_text)
|
selected_paths = []
|
||||||
self.dialog.selected_file = path
|
for item_id in selections:
|
||||||
self.dialog.update_status_bar(path)
|
path = self.dialog.item_path_map.get(item_id)
|
||||||
self.dialog.search_manager.show_search_ready()
|
if path:
|
||||||
if not os.path.isdir(self.dialog.selected_file):
|
selected_paths.append(path)
|
||||||
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
self.dialog.result = selected_paths
|
||||||
self.dialog.widget_manager.filename_entry.insert(0, item_text)
|
self.dialog.update_selection_info()
|
||||||
|
else:
|
||||||
|
item_id = selections[0]
|
||||||
|
path = self.dialog.item_path_map.get(item_id)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.dialog.dialog_mode == 'dir' and not self._is_dir(path):
|
||||||
|
self.dialog.result = None
|
||||||
|
self.dialog.tree.selection_remove(item_id)
|
||||||
|
self.dialog.update_selection_info()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.dialog.update_selection_info(path)
|
||||||
|
|
||||||
def on_list_context_menu(self, event: tk.Event) -> str:
|
def on_list_context_menu(self, event: tk.Event) -> str:
|
||||||
"""Shows the context menu for a list view item."""
|
"""
|
||||||
|
Shows the context menu for a list view item.
|
||||||
|
"""
|
||||||
iid = self.dialog.tree.identify_row(event.y)
|
iid = self.dialog.tree.identify_row(event.y)
|
||||||
if not iid:
|
if not iid:
|
||||||
return "break"
|
return "break"
|
||||||
self.dialog.tree.selection_set(iid)
|
self.dialog.tree.selection_set(iid)
|
||||||
item_text = self.dialog.tree.item(iid, "text").strip()
|
path = self.dialog.item_path_map.get(iid)
|
||||||
item_path = os.path.join(self.dialog.current_dir, item_text)
|
if path:
|
||||||
self.dialog.file_op_manager._show_context_menu(event, item_path)
|
self.dialog.file_op_manager._show_context_menu(event, path)
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
def _handle_item_double_click(self, path: str) -> None:
|
def _handle_item_double_click(self, path: str) -> None:
|
||||||
"""
|
"""
|
||||||
Handles the logic for a double-click on any item, regardless of view.
|
Handles the logic for a double-click on any item, regardless of view.
|
||||||
Navigates into directories or selects files.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): The full path of the double-clicked item.
|
|
||||||
"""
|
"""
|
||||||
if os.path.isdir(path):
|
if self._is_dir(path):
|
||||||
self.dialog.navigation_manager.navigate_to(path)
|
if self.dialog.dialog_mode == 'dir':
|
||||||
elif self.dialog.dialog_mode == "open":
|
has_subdirs = False
|
||||||
self.dialog.selected_file = path
|
try:
|
||||||
self.dialog.destroy()
|
if self.dialog.current_fs_type == "sftp":
|
||||||
|
import stat
|
||||||
|
items, _ = self.dialog.sftp_manager.list_directory(path)
|
||||||
|
for item in items:
|
||||||
|
if item.filename not in ['.', '..'] and stat.S_ISDIR(item.st_mode):
|
||||||
|
has_subdirs = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
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"],
|
||||||
|
LocaleStrings.CFD["enter_button"],
|
||||||
|
LocaleStrings.CFD["cancel_button"],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
choice = dialog.show()
|
||||||
|
|
||||||
|
if choice is True:
|
||||||
|
self.dialog.result = path
|
||||||
|
self.dialog.on_open()
|
||||||
|
elif choice is False:
|
||||||
|
self.dialog.navigation_manager.navigate_to(path)
|
||||||
|
else:
|
||||||
|
self.dialog.navigation_manager.navigate_to(path)
|
||||||
|
|
||||||
|
elif self.dialog.dialog_mode in ["open", "multi"]:
|
||||||
|
self.dialog.result = path
|
||||||
|
self.dialog.on_open()
|
||||||
elif self.dialog.dialog_mode == "save":
|
elif self.dialog.dialog_mode == "save":
|
||||||
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
||||||
self.dialog.widget_manager.filename_entry.insert(
|
self.dialog.widget_manager.filename_entry.insert(
|
||||||
@@ -505,24 +603,24 @@ class ViewManager:
|
|||||||
self.dialog.on_save()
|
self.dialog.on_save()
|
||||||
|
|
||||||
def on_list_double_click(self, event: tk.Event) -> None:
|
def on_list_double_click(self, event: tk.Event) -> None:
|
||||||
"""Handles a double-click on a list view item."""
|
"""
|
||||||
if not self.dialog.tree.selection():
|
Handles a double-click on a list view item.
|
||||||
|
"""
|
||||||
|
selection = self.dialog.tree.selection()
|
||||||
|
if not selection:
|
||||||
return
|
return
|
||||||
item_id = self.dialog.tree.selection()[0]
|
item_id = selection[0]
|
||||||
item_text = self.dialog.tree.item(item_id, 'text').strip()
|
path = self.dialog.item_path_map.get(item_id)
|
||||||
path = os.path.join(self.dialog.current_dir, item_text)
|
if path:
|
||||||
self._handle_item_double_click(path)
|
self._handle_item_double_click(path)
|
||||||
|
|
||||||
def _select_file_in_view(self, filename: str) -> None:
|
def _select_file_in_view(self, filename: str) -> None:
|
||||||
"""
|
"""
|
||||||
Programmatically selects a file in the current view.
|
Programmatically selects a file in the current view.
|
||||||
|
|
||||||
Args:
|
|
||||||
filename (str): The name of the file to select.
|
|
||||||
"""
|
"""
|
||||||
if self.dialog.view_mode.get() == "list":
|
if self.dialog.view_mode.get() == "list":
|
||||||
for item_id in self.dialog.tree.get_children():
|
for item_id, path in self.dialog.item_path_map.items():
|
||||||
if self.dialog.tree.item(item_id, "text").strip() == filename:
|
if os.path.basename(path) == filename:
|
||||||
self.dialog.tree.selection_set(item_id)
|
self.dialog.tree.selection_set(item_id)
|
||||||
self.dialog.tree.focus(item_id)
|
self.dialog.tree.focus(item_id)
|
||||||
self.dialog.tree.see(item_id)
|
self.dialog.tree.see(item_id)
|
||||||
@@ -610,4 +708,4 @@ class ViewManager:
|
|||||||
"""Unbinds all mouse wheel events from the dialog."""
|
"""Unbinds all mouse wheel events from the dialog."""
|
||||||
self.dialog.unbind_all("<MouseWheel>")
|
self.dialog.unbind_all("<MouseWheel>")
|
||||||
self.dialog.unbind_all("<Button-4>")
|
self.dialog.unbind_all("<Button-4>")
|
||||||
self.dialog.unbind_all("<Button-5>")
|
self.dialog.unbind_all("<Button-5>")
|
@@ -4,8 +4,13 @@ import tkinter as tk
|
|||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
|
import webbrowser
|
||||||
from typing import Optional, List, Tuple, Dict, Union
|
from typing import Optional, List, Tuple, Dict, Union
|
||||||
from shared_libs.common_tools import IconManager, Tooltip, LxTools
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from shared_libs.common_tools import IconManager, Tooltip, LxTools, message_box_animation
|
||||||
|
from shared_libs.gitea import GiteaUpdater, GiteaApiUrlError, GiteaVersionParseError, GiteaApiResponseError
|
||||||
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
||||||
from .cfd_ui_setup import StyleManager, WidgetManager
|
from .cfd_ui_setup import StyleManager, WidgetManager
|
||||||
from shared_libs.animated_icon import AnimatedIcon
|
from shared_libs.animated_icon import AnimatedIcon
|
||||||
@@ -14,6 +19,8 @@ from .cfd_file_operations import FileOperationsManager
|
|||||||
from .cfd_search_manager import SearchManager
|
from .cfd_search_manager import SearchManager
|
||||||
from .cfd_navigation_manager import NavigationManager
|
from .cfd_navigation_manager import NavigationManager
|
||||||
from .cfd_view_manager import ViewManager
|
from .cfd_view_manager import ViewManager
|
||||||
|
from .cfd_sftp_manager import SFTPManager, PARAMIKO_AVAILABLE
|
||||||
|
from shared_libs.message import CredentialsDialog, MessageDialog, InputDialog
|
||||||
|
|
||||||
|
|
||||||
class CustomFileDialog(tk.Toplevel):
|
class CustomFileDialog(tk.Toplevel):
|
||||||
@@ -22,35 +29,23 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
directory navigation, search, and file operations.
|
directory navigation, search, and file operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _initialize_managers(self) -> None:
|
|
||||||
"""Initializes or re-initializes all the manager classes."""
|
|
||||||
self.style_manager: StyleManager = StyleManager(self)
|
|
||||||
self.file_op_manager: FileOperationsManager = FileOperationsManager(self)
|
|
||||||
self.search_manager: SearchManager = SearchManager(self)
|
|
||||||
self.navigation_manager: NavigationManager = NavigationManager(self)
|
|
||||||
self.view_manager: ViewManager = ViewManager(self)
|
|
||||||
self.widget_manager: WidgetManager = WidgetManager(self, self.settings)
|
|
||||||
|
|
||||||
def __init__(self, parent: tk.Widget, initial_dir: Optional[str] = None,
|
def __init__(self, parent: tk.Widget, initial_dir: Optional[str] = None,
|
||||||
filetypes: Optional[List[Tuple[str, str]]] = None,
|
filetypes: Optional[List[Tuple[str, str]]] = None,
|
||||||
mode: str = "open", title: str = LocaleStrings.CFD["title"]):
|
mode: str = "open", title: str = LocaleStrings.CFD["title"]):
|
||||||
"""
|
"""
|
||||||
Initializes the CustomFileDialog.
|
Initializes the CustomFileDialog.
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: The parent widget.
|
|
||||||
initial_dir: The initial directory to display.
|
|
||||||
filetypes: A list of filetype tuples, e.g., [('Text files', '*.txt')].
|
|
||||||
mode: The dialog mode. Can be "open" (select single file),
|
|
||||||
"save" (select single file for saving),
|
|
||||||
"multi" (select multiple files/directories),
|
|
||||||
or "dir" (select a single directory).
|
|
||||||
title: The title of the dialog window.
|
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self.current_fs_type = "local" # "local" or "sftp"
|
||||||
|
|
||||||
|
self.sftp_manager = SFTPManager()
|
||||||
|
self.config_manager = CfdConfigManager()
|
||||||
|
|
||||||
self.my_tool_tip: Optional[Tooltip] = None
|
self.my_tool_tip: Optional[Tooltip] = None
|
||||||
self.dialog_mode: str = mode
|
self.dialog_mode: str = mode
|
||||||
|
self.gitea_api_url = CfdConfigManager.UPDATE_URL
|
||||||
|
self.lib_version = CfdConfigManager.VERSION
|
||||||
|
self.update_status: str = ""
|
||||||
|
|
||||||
self.load_settings()
|
self.load_settings()
|
||||||
|
|
||||||
@@ -81,6 +76,7 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.show_hidden_files: tk.BooleanVar = tk.BooleanVar(value=False)
|
self.show_hidden_files: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||||
self.resize_job: Optional[str] = None
|
self.resize_job: Optional[str] = None
|
||||||
self.last_width: int = 0
|
self.last_width: int = 0
|
||||||
|
self.selected_item_frames: List[ttk.Frame] = []
|
||||||
self.search_results: List[str] = []
|
self.search_results: List[str] = []
|
||||||
self.search_mode: bool = False
|
self.search_mode: bool = False
|
||||||
self.original_path_text: str = ""
|
self.original_path_text: str = ""
|
||||||
@@ -113,11 +109,28 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.widget_manager.path_entry.bind(
|
self.widget_manager.path_entry.bind(
|
||||||
"<Return>", self.navigation_manager.handle_path_entry_return)
|
"<Return>", self.navigation_manager.handle_path_entry_return)
|
||||||
|
|
||||||
|
self.widget_manager.home_button.config(command=self.go_to_local_home)
|
||||||
|
|
||||||
self.bind("<Key>", self.search_manager.show_search_bar)
|
self.bind("<Key>", self.search_manager.show_search_bar)
|
||||||
|
|
||||||
if self.dialog_mode == "save":
|
if self.dialog_mode == "save":
|
||||||
self.bind("<Delete>", self.file_op_manager.delete_selected_item)
|
self.bind("<Delete>", self.file_op_manager.delete_selected_item)
|
||||||
|
|
||||||
|
if self.gitea_api_url and self.lib_version:
|
||||||
|
self.update_thread = threading.Thread(
|
||||||
|
target=self.check_for_updates, daemon=True)
|
||||||
|
self.update_thread.start()
|
||||||
|
|
||||||
|
def _initialize_managers(self) -> None:
|
||||||
|
"""Initializes or re-initializes all the manager classes."""
|
||||||
|
self.style_manager: StyleManager = StyleManager(self)
|
||||||
|
self.file_op_manager: FileOperationsManager = FileOperationsManager(
|
||||||
|
self)
|
||||||
|
self.search_manager: SearchManager = SearchManager(self)
|
||||||
|
self.navigation_manager: NavigationManager = NavigationManager(self)
|
||||||
|
self.view_manager: ViewManager = ViewManager(self)
|
||||||
|
self.widget_manager: WidgetManager = WidgetManager(self, self.settings)
|
||||||
|
|
||||||
def load_settings(self) -> None:
|
def load_settings(self) -> None:
|
||||||
"""Loads settings from the configuration file."""
|
"""Loads settings from the configuration file."""
|
||||||
self.settings = CfdConfigManager.load()
|
self.settings = CfdConfigManager.load()
|
||||||
@@ -129,12 +142,6 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def get_min_size_from_preset(self, preset: str) -> Tuple[int, int]:
|
def get_min_size_from_preset(self, preset: str) -> Tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
Calculates the minimum window size based on a preset string.
|
Calculates the minimum window size based on a preset string.
|
||||||
|
|
||||||
Args:
|
|
||||||
preset: The size preset string (e.g., "1050x850").
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A tuple containing the minimum width and height.
|
|
||||||
"""
|
"""
|
||||||
w, h = map(int, preset.split('x'))
|
w, h = map(int, preset.split('x'))
|
||||||
return max(650, w - 400), max(450, h - 400)
|
return max(650, w - 400), max(450, h - 400)
|
||||||
@@ -175,6 +182,89 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
"""Opens the settings dialog."""
|
"""Opens the settings dialog."""
|
||||||
SettingsDialog(self, dialog_mode=self.dialog_mode)
|
SettingsDialog(self, dialog_mode=self.dialog_mode)
|
||||||
|
|
||||||
|
def open_sftp_dialog(self):
|
||||||
|
if not PARAMIKO_AVAILABLE:
|
||||||
|
MessageDialog(message_type="error",
|
||||||
|
text="Paramiko library is not installed.").show()
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog = CredentialsDialog(self)
|
||||||
|
credentials = dialog.show()
|
||||||
|
|
||||||
|
if credentials:
|
||||||
|
self.connect_sftp(credentials, is_new_connection=True)
|
||||||
|
|
||||||
|
def connect_sftp(self, credentials, is_new_connection: bool = False):
|
||||||
|
self.config(cursor="watch")
|
||||||
|
self.update_idletasks()
|
||||||
|
|
||||||
|
success, message = self.sftp_manager.connect(
|
||||||
|
host=credentials['host'],
|
||||||
|
port=credentials['port'],
|
||||||
|
username=credentials['username'],
|
||||||
|
password=credentials['password'],
|
||||||
|
key_file=credentials['key_file'],
|
||||||
|
passphrase=credentials['passphrase']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.config(cursor="")
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.current_fs_type = "sftp"
|
||||||
|
self.widget_manager.sftp_button.config(
|
||||||
|
command=self.disconnect_sftp, style="Header.TButton.Active.Round")
|
||||||
|
|
||||||
|
initial_path = credentials.get("initial_path", "/")
|
||||||
|
self.navigation_manager.navigate_to(initial_path)
|
||||||
|
|
||||||
|
if is_new_connection:
|
||||||
|
save_bookmark = MessageDialog(
|
||||||
|
message_type="ask",
|
||||||
|
text="Connection successful. Save as bookmark?",
|
||||||
|
buttons=["Yes", "No"]
|
||||||
|
).show()
|
||||||
|
|
||||||
|
if save_bookmark:
|
||||||
|
bookmark_name = InputDialog(
|
||||||
|
self,
|
||||||
|
title="Save Bookmark",
|
||||||
|
prompt="Enter a name for the bookmark:",
|
||||||
|
initial_value=credentials['host']
|
||||||
|
).show()
|
||||||
|
|
||||||
|
if bookmark_name:
|
||||||
|
self.config_manager.add_bookmark(
|
||||||
|
bookmark_name, credentials)
|
||||||
|
self.reload_config_and_rebuild_ui()
|
||||||
|
else:
|
||||||
|
MessageDialog(message_type="error",
|
||||||
|
text=f"Connection failed: {message}").show()
|
||||||
|
|
||||||
|
def connect_sftp_bookmark(self, data):
|
||||||
|
self.connect_sftp(data, is_new_connection=False)
|
||||||
|
|
||||||
|
def remove_sftp_bookmark(self, name):
|
||||||
|
confirm = MessageDialog(
|
||||||
|
message_type="ask",
|
||||||
|
text=f"Remove bookmark '{name}'?",
|
||||||
|
buttons=["Yes", "No"]).show()
|
||||||
|
if confirm:
|
||||||
|
self.config_manager.remove_bookmark(name)
|
||||||
|
self.reload_config_and_rebuild_ui()
|
||||||
|
|
||||||
|
def disconnect_sftp(self):
|
||||||
|
self.sftp_manager.disconnect()
|
||||||
|
self.current_fs_type = "local"
|
||||||
|
self.widget_manager.sftp_button.config(
|
||||||
|
command=self.open_sftp_dialog, style="Header.TButton.Borderless.Round")
|
||||||
|
self.navigation_manager.navigate_to(os.path.expanduser("~"))
|
||||||
|
|
||||||
|
def go_to_local_home(self):
|
||||||
|
if self.current_fs_type == "sftp":
|
||||||
|
self.disconnect_sftp()
|
||||||
|
else:
|
||||||
|
self.navigation_manager.navigate_to(os.path.expanduser("~"))
|
||||||
|
|
||||||
def update_animation_settings(self) -> None:
|
def update_animation_settings(self) -> None:
|
||||||
"""Updates the search animation icon based on current settings."""
|
"""Updates the search animation icon based on current settings."""
|
||||||
use_pillow = self.settings.get('use_pillow_animation', False)
|
use_pillow = self.settings.get('use_pillow_animation', False)
|
||||||
@@ -207,16 +297,57 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
if is_running:
|
if is_running:
|
||||||
self.widget_manager.search_animation.start()
|
self.widget_manager.search_animation.start()
|
||||||
|
|
||||||
|
def check_for_updates(self) -> None:
|
||||||
|
"""Checks for library updates via the Gitea API in a background thread."""
|
||||||
|
try:
|
||||||
|
new_version = GiteaUpdater.check_for_update(
|
||||||
|
self.gitea_api_url,
|
||||||
|
self.lib_version,
|
||||||
|
)
|
||||||
|
self.after(0, self.update_ui_for_update, new_version)
|
||||||
|
except (requests.exceptions.RequestException, GiteaApiUrlError, GiteaVersionParseError, GiteaApiResponseError):
|
||||||
|
self.after(0, self.update_ui_for_update, "ERROR")
|
||||||
|
except Exception:
|
||||||
|
self.after(0, self.update_ui_for_update, "ERROR")
|
||||||
|
|
||||||
|
def _run_installer(self, event: Optional[tk.Event] = None) -> None:
|
||||||
|
"""Runs the LxTools installer if it exists."""
|
||||||
|
installer_path = '/usr/local/bin/lxtools_installer'
|
||||||
|
if os.path.exists(installer_path):
|
||||||
|
try:
|
||||||
|
subprocess.Popen([installer_path])
|
||||||
|
self.widget_manager.search_status_label.config(
|
||||||
|
text="Installer started...")
|
||||||
|
except OSError as e:
|
||||||
|
self.widget_manager.search_status_label.config(
|
||||||
|
text=f"Error starting installer: {e}")
|
||||||
|
else:
|
||||||
|
self.widget_manager.search_status_label.config(
|
||||||
|
text=f"Installer not found at {installer_path}")
|
||||||
|
|
||||||
|
def update_ui_for_update(self, new_version: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
Updates the UI based on the result of the library update check.
|
||||||
|
"""
|
||||||
|
self.update_status = new_version
|
||||||
|
icon = self.widget_manager.update_animation_icon
|
||||||
|
icon.grid_remove()
|
||||||
|
icon.hide()
|
||||||
|
|
||||||
|
if new_version is None or new_version == "ERROR":
|
||||||
|
return
|
||||||
|
|
||||||
|
icon.grid(row=0, column=2, sticky='e', padx=(10, 5))
|
||||||
|
tooltip_msg = LocaleStrings.UI["install_new_version"].format(
|
||||||
|
version=new_version)
|
||||||
|
icon.start()
|
||||||
|
|
||||||
|
icon.bind("<Button-1>", self._run_installer)
|
||||||
|
Tooltip(icon, tooltip_msg)
|
||||||
|
|
||||||
def get_file_icon(self, filename: str, size: str = 'large') -> tk.PhotoImage:
|
def get_file_icon(self, filename: str, size: str = 'large') -> tk.PhotoImage:
|
||||||
"""
|
"""
|
||||||
Gets the appropriate icon for a given filename.
|
Gets the appropriate icon for a given filename.
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The name of the file.
|
|
||||||
size: The desired icon size ('large' or 'small').
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A PhotoImage object for the corresponding file type.
|
|
||||||
"""
|
"""
|
||||||
ext = os.path.splitext(filename)[1].lower()
|
ext = os.path.splitext(filename)[1].lower()
|
||||||
|
|
||||||
@@ -240,9 +371,6 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def on_window_resize(self, event: tk.Event) -> None:
|
def on_window_resize(self, event: tk.Event) -> None:
|
||||||
"""
|
"""
|
||||||
Handles the window resize event.
|
Handles the window resize event.
|
||||||
|
|
||||||
Args:
|
|
||||||
event: The event object.
|
|
||||||
"""
|
"""
|
||||||
if event.widget is self:
|
if event.widget is self:
|
||||||
if self.view_mode.get() == "icons" and not self.search_mode:
|
if self.view_mode.get() == "icons" and not self.search_mode:
|
||||||
@@ -264,9 +392,6 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def _handle_responsive_buttons(self, window_width: int) -> None:
|
def _handle_responsive_buttons(self, window_width: int) -> None:
|
||||||
"""
|
"""
|
||||||
Shows or hides buttons based on the window width.
|
Shows or hides buttons based on the window width.
|
||||||
|
|
||||||
Args:
|
|
||||||
window_width: The current width of the window.
|
|
||||||
"""
|
"""
|
||||||
threshold = 850
|
threshold = 850
|
||||||
container = self.widget_manager.responsive_buttons_container
|
container = self.widget_manager.responsive_buttons_container
|
||||||
@@ -317,9 +442,6 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def on_sidebar_resize(self, event: tk.Event) -> None:
|
def on_sidebar_resize(self, event: tk.Event) -> None:
|
||||||
"""
|
"""
|
||||||
Handles the sidebar resize event, adjusting button text visibility.
|
Handles the sidebar resize event, adjusting button text visibility.
|
||||||
|
|
||||||
Args:
|
|
||||||
event: The event object.
|
|
||||||
"""
|
"""
|
||||||
current_width = event.width
|
current_width = event.width
|
||||||
threshold_width = 100
|
threshold_width = 100
|
||||||
@@ -338,9 +460,6 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def _on_devices_enter(self, event: tk.Event) -> None:
|
def _on_devices_enter(self, event: tk.Event) -> None:
|
||||||
"""
|
"""
|
||||||
Shows the scrollbar when the mouse enters the devices area.
|
Shows the scrollbar when the mouse enters the devices area.
|
||||||
|
|
||||||
Args:
|
|
||||||
event: The event object.
|
|
||||||
"""
|
"""
|
||||||
self.widget_manager.devices_scrollbar.grid(
|
self.widget_manager.devices_scrollbar.grid(
|
||||||
row=1, column=1, sticky="ns")
|
row=1, column=1, sticky="ns")
|
||||||
@@ -348,9 +467,6 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def _on_devices_leave(self, event: tk.Event) -> None:
|
def _on_devices_leave(self, event: tk.Event) -> None:
|
||||||
"""
|
"""
|
||||||
Hides the scrollbar when the mouse leaves the devices area.
|
Hides the scrollbar when the mouse leaves the devices area.
|
||||||
|
|
||||||
Args:
|
|
||||||
event: The event object.
|
|
||||||
"""
|
"""
|
||||||
x, y = event.x_root, event.y_root
|
x, y = event.x_root, event.y_root
|
||||||
widget_x = self.widget_manager.devices_canvas.winfo_rootx()
|
widget_x = self.widget_manager.devices_canvas.winfo_rootx()
|
||||||
@@ -375,33 +491,83 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.widget_manager.recursive_button.configure(
|
self.widget_manager.recursive_button.configure(
|
||||||
style="Header.TButton.Borderless.Round")
|
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.
|
||||||
|
"""
|
||||||
|
self._update_disk_usage()
|
||||||
|
status_text = ""
|
||||||
|
|
||||||
Args:
|
if self.dialog_mode == 'multi':
|
||||||
status_info: The path of the currently selected item or a custom string.
|
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:
|
||||||
|
status_text = ""
|
||||||
|
else:
|
||||||
|
if status_info and (self.current_fs_type == 'sftp' or 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 self.view_manager._is_dir(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:
|
||||||
|
status_text = f"'{os.path.basename(status_info)}'"
|
||||||
|
elif status_info:
|
||||||
|
status_text = status_info
|
||||||
|
|
||||||
|
self.widget_manager.search_status_label.config(text=status_text)
|
||||||
|
self.update_action_buttons_state()
|
||||||
|
|
||||||
|
def _update_disk_usage(self) -> None:
|
||||||
|
"""Updates only the disk usage part of the status bar."""
|
||||||
|
if self.current_fs_type == "sftp":
|
||||||
|
self.widget_manager.storage_label.config(text="SFTP Storage: N/A")
|
||||||
|
self.widget_manager.storage_bar['value'] = 0
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
total, used, free = shutil.disk_usage(self.current_dir)
|
total, used, free = shutil.disk_usage(self.current_dir)
|
||||||
free_str = self._format_size(free)
|
free_str = self._format_size(free)
|
||||||
self.widget_manager.storage_label.config(
|
self.widget_manager.storage_label.config(
|
||||||
text=f"{LocaleStrings.CFD['free_space']}: {free_str}")
|
text=f"{LocaleStrings.CFD['free_space']}: {free_str}")
|
||||||
self.widget_manager.storage_bar['value'] = (used / total) * 100
|
self.widget_manager.storage_bar['value'] = (used / total) * 100
|
||||||
|
except FileNotFoundError:
|
||||||
status_text = ""
|
self.widget_manager.storage_label.config(
|
||||||
if status_info and os.path.exists(status_info):
|
text=f"{LocaleStrings.CFD['free_space']}: {LocaleStrings.CFD['unknown']}")
|
||||||
selected_path = status_info
|
self.widget_manager.storage_bar['value'] = 0
|
||||||
if os.path.isdir(selected_path):
|
self.widget_manager.search_status_label.config(
|
||||||
content_count = self.view_manager._get_folder_content_count(
|
text=LocaleStrings.CFD["directory_not_found"])
|
||||||
selected_path)
|
|
||||||
if content_count is not None:
|
|
||||||
status_text = f
|
|
||||||
|
|
||||||
def on_open(self) -> None:
|
def on_open(self) -> None:
|
||||||
"""Handles the 'Open' action, closing the dialog if a file is selected."""
|
"""Handles the 'Open' or 'OK' action based on the dialog mode."""
|
||||||
if self.result and isinstance(self.result, str) and os.path.isfile(self.result):
|
if self.dialog_mode == 'multi':
|
||||||
self.destroy()
|
if self.result and isinstance(self.result, list) and self.result:
|
||||||
|
self.destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
selected_path = self.result
|
||||||
|
if not selected_path or not isinstance(selected_path, str):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.dialog_mode == 'dir':
|
||||||
|
if self.view_manager._is_dir(selected_path):
|
||||||
|
self.destroy()
|
||||||
|
elif self.dialog_mode == 'open':
|
||||||
|
if not self.view_manager._is_dir(selected_path):
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
def on_save(self) -> None:
|
def on_save(self) -> None:
|
||||||
"""Handles the 'Save' action, setting the selected file and closing the dialog."""
|
"""Handles the 'Save' action, setting the selected file and closing the dialog."""
|
||||||
@@ -416,32 +582,42 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def get_result(self) -> Optional[Union[str, List[str]]]:
|
def get_result(self) -> Optional[Union[str, List[str]]]:
|
||||||
"""
|
"""Returns the result of the dialog."""
|
||||||
Returns the result of the dialog.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- A string containing a single path for modes 'open', 'save', 'dir'.
|
|
||||||
- A list of strings for mode 'multi'.
|
|
||||||
- None if the dialog was cancelled.
|
|
||||||
"""
|
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
def update_action_buttons_state(self) -> None:
|
def update_action_buttons_state(self) -> None:
|
||||||
"""Updates the state of action buttons (e.g., 'New Folder') based on directory permissions."""
|
"""Updates the state of action buttons based on current context."""
|
||||||
is_writable = os.access(self.current_dir, os.W_OK)
|
new_folder_state = tk.DISABLED
|
||||||
state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED
|
new_file_state = tk.DISABLED
|
||||||
self.widget_manager.new_folder_button.config(state=state)
|
trash_state = tk.DISABLED
|
||||||
self.widget_manager.new_file_button.config(state=state)
|
|
||||||
|
is_writable = False
|
||||||
|
if self.dialog_mode != "open":
|
||||||
|
if self.current_fs_type == 'sftp':
|
||||||
|
is_writable = True
|
||||||
|
else:
|
||||||
|
is_writable = os.access(self.current_dir, os.W_OK)
|
||||||
|
|
||||||
|
if is_writable:
|
||||||
|
new_folder_state = tk.NORMAL
|
||||||
|
new_file_state = tk.NORMAL
|
||||||
|
|
||||||
|
if self.dialog_mode == "save":
|
||||||
|
trash_state = tk.NORMAL
|
||||||
|
|
||||||
|
if hasattr(self.widget_manager, 'new_folder_button'):
|
||||||
|
self.widget_manager.new_folder_button.config(
|
||||||
|
state=new_folder_state)
|
||||||
|
|
||||||
|
if hasattr(self.widget_manager, 'new_file_button'):
|
||||||
|
self.widget_manager.new_file_button.config(state=new_file_state)
|
||||||
|
|
||||||
|
if hasattr(self.widget_manager, 'trash_button'):
|
||||||
|
self.widget_manager.trash_button.config(state=trash_state)
|
||||||
|
|
||||||
def _matches_filetype(self, filename: str) -> bool:
|
def _matches_filetype(self, filename: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if a filename matches the current filetype filter.
|
Checks if a filename matches the current filetype filter.
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The name of the file to check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the file matches the filter, False otherwise.
|
|
||||||
"""
|
"""
|
||||||
if self.current_filter_pattern == "*.*":
|
if self.current_filter_pattern == "*.*":
|
||||||
return True
|
return True
|
||||||
@@ -464,12 +640,6 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def _format_size(self, size_bytes: Optional[int]) -> str:
|
def _format_size(self, size_bytes: Optional[int]) -> str:
|
||||||
"""
|
"""
|
||||||
Formats a size in bytes into a human-readable string (KB, MB, GB).
|
Formats a size in bytes into a human-readable string (KB, MB, GB).
|
||||||
|
|
||||||
Args:
|
|
||||||
size_bytes: The size in bytes.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A formatted string representing the size.
|
|
||||||
"""
|
"""
|
||||||
if size_bytes is None:
|
if size_bytes is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -484,23 +654,12 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
def shorten_text(self, text: str, max_len: int) -> str:
|
def shorten_text(self, text: str, max_len: int) -> str:
|
||||||
"""
|
"""
|
||||||
Shortens a string to a maximum length, adding '...' if truncated.
|
Shortens a string to a maximum length, adding '...' if truncated.
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The text to shorten.
|
|
||||||
max_len: The maximum allowed length.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The shortened text.
|
|
||||||
"""
|
"""
|
||||||
return text if len(text) <= max_len else text[:max_len-3] + "..."
|
return text if len(text) <= max_len else text[:max_len-3] + "..."
|
||||||
|
|
||||||
def _get_mounted_devices(self) -> List[Tuple[str, str, bool]]:
|
def _get_mounted_devices(self) -> List[Tuple[str, str, bool]]:
|
||||||
"""
|
"""
|
||||||
Retrieves a list of mounted devices on the system.
|
Retrieves a list of mounted devices on the system.
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of tuples, where each tuple contains the display name,
|
|
||||||
mount point, and a boolean indicating if it's removable.
|
|
||||||
"""
|
"""
|
||||||
devices: List[Tuple[str, str, bool]] = []
|
devices: List[Tuple[str, str, bool]] = []
|
||||||
root_disk_name: Optional[str] = None
|
root_disk_name: Optional[str] = None
|
||||||
@@ -519,7 +678,8 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
break
|
break
|
||||||
|
|
||||||
for block_device in data.get('blockdevices', []):
|
for block_device in data.get('blockdevices', []):
|
||||||
if (block_device.get('mountpoint') and
|
if (
|
||||||
|
block_device.get('mountpoint') and
|
||||||
block_device.get('type') not in ['loop', 'rom'] and
|
block_device.get('type') not in ['loop', 'rom'] and
|
||||||
block_device.get('mountpoint') != '/'):
|
block_device.get('mountpoint') != '/'):
|
||||||
|
|
||||||
@@ -536,7 +696,8 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
|
|
||||||
if 'children' in block_device:
|
if 'children' in block_device:
|
||||||
for child_device in block_device['children']:
|
for child_device in block_device['children']:
|
||||||
if (child_device.get('mountpoint') and
|
if (
|
||||||
|
child_device.get('mountpoint') and
|
||||||
child_device.get('type') not in ['loop', 'rom'] and
|
child_device.get('type') not in ['loop', 'rom'] and
|
||||||
child_device.get('mountpoint') != '/'):
|
child_device.get('mountpoint') != '/'):
|
||||||
|
|
||||||
@@ -554,4 +715,4 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting mounted devices: {e}")
|
print(f"Error getting mounted devices: {e}")
|
||||||
return devices
|
return devices
|
||||||
|
426
message.py
426
message.py
@@ -12,95 +12,7 @@ except (ModuleNotFoundError, NameError):
|
|||||||
class MessageDialog:
|
class MessageDialog:
|
||||||
"""
|
"""
|
||||||
A customizable message dialog window using tkinter for user interaction.
|
A customizable message dialog window using tkinter for user interaction.
|
||||||
|
|
||||||
This class creates modal dialogs for displaying information, warnings, errors,
|
|
||||||
or questions to the user. It supports multiple button configurations, custom
|
|
||||||
icons, keyboard navigation, and command binding. The dialog is centered on the
|
|
||||||
screen and handles user interactions with focus management and accessibility.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
message_type (str): Type of message ("info", "error", "warning", "ask").
|
|
||||||
text (str): Main message content to display.
|
|
||||||
buttons (List[str]): List of button labels (e.g., ["OK", "Cancel"]).
|
|
||||||
result (bool or None):
|
|
||||||
- True for positive actions (Yes, OK)
|
|
||||||
- False for negative actions (No, Cancel)
|
|
||||||
- None if "Cancel" was clicked with ≥3 buttons
|
|
||||||
icons: Dictionary mapping message types to tkinter.PhotoImage objects
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
message_type: Type of message dialog (default: "info")
|
|
||||||
text: Message content to display
|
|
||||||
buttons: List of button labels (default: ["OK"])
|
|
||||||
master: Parent tkinter window (optional)
|
|
||||||
commands: List of callables for each button (default: [None])
|
|
||||||
icon: Custom icon path (overrides default icons if provided)
|
|
||||||
title: Window title (default: derived from message_type)
|
|
||||||
font: Font tuple for text styling
|
|
||||||
wraplength: Text wrapping width in pixels
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
_get_title(): Returns the default window title based on message type.
|
|
||||||
_on_button_click(button_text): Sets result and closes the dialog.
|
|
||||||
show(): Displays the dialog and waits for user response.
|
|
||||||
|
|
||||||
Example Usage:
|
|
||||||
|
|
||||||
1. Basic Info Dialog:
|
|
||||||
>>> MessageDialog(
|
|
||||||
... text="This is an information message.")
|
|
||||||
>>> result = dialog.show()
|
|
||||||
>>> print("User clicked OK:", result)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
My Favorite Example,
|
|
||||||
for simply information message:
|
|
||||||
|
|
||||||
>>> MessageDialog(text="This is an information message.")
|
|
||||||
>>> result = MessageDialog(text="This is an information message.").show()
|
|
||||||
|
|
||||||
Example Usage:
|
|
||||||
|
|
||||||
2. Error Dialog with Custom Command:
|
|
||||||
>>> def on_retry():
|
|
||||||
... print("User selected Retry")
|
|
||||||
|
|
||||||
>>> dialog = MessageDialog(
|
|
||||||
... message_type="error",
|
|
||||||
... text="An error occurred during processing.",
|
|
||||||
... buttons=["Retry", "Cancel"],
|
|
||||||
... commands=[on_retry, None],
|
|
||||||
... title="Critical Error"
|
|
||||||
... )
|
|
||||||
>>> result = dialog.show()
|
|
||||||
>>> print("User selected Retry:", result)
|
|
||||||
|
|
||||||
Example Usage:
|
|
||||||
|
|
||||||
3. And a special example for a "open link" button:
|
|
||||||
Be careful not to forget to import it into the script in which
|
|
||||||
this dialog is used!!! import webbrowser from functools import partial
|
|
||||||
|
|
||||||
>>> MessageDialog(
|
|
||||||
... "info"
|
|
||||||
... text="This is an information message.",
|
|
||||||
... buttons=["Yes", "Go to Exapmle"],
|
|
||||||
... commands=[
|
|
||||||
... None, # Default on "OK"
|
|
||||||
... partial(webbrowser.open, "https://exapmle.com"),
|
|
||||||
... ],
|
|
||||||
... icon="/pathh/to/custom/icon.png",
|
|
||||||
... title="Example",
|
|
||||||
... )
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Returns None if "Cancel" was clicked with ≥3 buttons
|
|
||||||
- Supports keyboard navigation (Left/Right arrows and Enter)
|
|
||||||
- Dialog automatically centers on screen
|
|
||||||
- Result is False for window close (X) with 2 buttons
|
|
||||||
- Font and wraplength parameters enable text styling
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message_type: str = "info",
|
message_type: str = "info",
|
||||||
@@ -113,23 +25,20 @@ class MessageDialog:
|
|||||||
font: tuple = None,
|
font: tuple = None,
|
||||||
wraplength: int = None,
|
wraplength: int = None,
|
||||||
):
|
):
|
||||||
self.message_type = message_type or "info" # Default is "info"
|
self.message_type = message_type or "info"
|
||||||
self.text = text
|
self.text = text
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
self.master = master
|
self.master = master
|
||||||
self.result: bool = False # Default is False
|
self.result: bool = False
|
||||||
|
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.title = title
|
self.title = title
|
||||||
# Window creation
|
|
||||||
self.window = tk.Toplevel(master)
|
self.window = tk.Toplevel(master)
|
||||||
self.window.grab_set()
|
|
||||||
self.window.resizable(False, False)
|
self.window.resizable(False, False)
|
||||||
ttk.Style().configure("TButton")
|
ttk.Style().configure("TButton")
|
||||||
self.buttons_widgets = []
|
self.buttons_widgets = []
|
||||||
self.current_button_index = 0
|
self.current_button_index = 0
|
||||||
|
|
||||||
# Load icons using IconManager
|
|
||||||
icon_manager = IconManager()
|
icon_manager = IconManager()
|
||||||
self.icons = {
|
self.icons = {
|
||||||
"error": icon_manager.get_icon("error_extralarge"),
|
"error": icon_manager.get_icon("error_extralarge"),
|
||||||
@@ -138,36 +47,27 @@ class MessageDialog:
|
|||||||
"ask": icon_manager.get_icon("question_mark_extralarge"),
|
"ask": icon_manager.get_icon("question_mark_extralarge"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Handle custom icon override
|
|
||||||
if self.icon:
|
if self.icon:
|
||||||
if isinstance(self.icon, str) and os.path.exists(self.icon):
|
if isinstance(self.icon, str) and os.path.exists(self.icon):
|
||||||
# If it's a path, load it
|
|
||||||
try:
|
try:
|
||||||
self.icons[self.message_type] = tk.PhotoImage(
|
self.icons[self.message_type] = tk.PhotoImage(file=self.icon)
|
||||||
file=self.icon)
|
|
||||||
except tk.TclError as e:
|
except tk.TclError as e:
|
||||||
print(
|
print(f"Error loading custom icon from path '{self.icon}': {e}")
|
||||||
f"Error loading custom icon from path '{self.icon}': {e}")
|
|
||||||
elif isinstance(self.icon, tk.PhotoImage):
|
elif isinstance(self.icon, tk.PhotoImage):
|
||||||
# If it's already a PhotoImage, use it directly
|
|
||||||
self.icons[self.message_type] = self.icon
|
self.icons[self.message_type] = self.icon
|
||||||
|
|
||||||
# Window title and icon
|
|
||||||
self.window.title(self._get_title() if not self.title else self.title)
|
self.window.title(self._get_title() if not self.title else self.title)
|
||||||
window_icon = self.icons.get(self.message_type)
|
window_icon = self.icons.get(self.message_type)
|
||||||
if window_icon:
|
if window_icon:
|
||||||
self.window.iconphoto(False, window_icon)
|
self.window.iconphoto(False, window_icon)
|
||||||
|
|
||||||
# Layout
|
|
||||||
frame = ttk.Frame(self.window)
|
frame = ttk.Frame(self.window)
|
||||||
frame.pack(expand=True, fill="both", padx=15, pady=8)
|
frame.pack(expand=True, fill="both", padx=15, pady=8)
|
||||||
|
|
||||||
# Grid-Configuration
|
|
||||||
frame.grid_rowconfigure(0, weight=1)
|
frame.grid_rowconfigure(0, weight=1)
|
||||||
frame.grid_columnconfigure(0, weight=1)
|
frame.grid_columnconfigure(0, weight=1)
|
||||||
frame.grid_columnconfigure(1, weight=3)
|
frame.grid_columnconfigure(1, weight=3)
|
||||||
|
|
||||||
# Icon and Text
|
|
||||||
icon_label = ttk.Label(frame, image=self.icons.get(self.message_type))
|
icon_label = ttk.Label(frame, image=self.icons.get(self.message_type))
|
||||||
pady_value = 5 if self.icon is not None else 15
|
pady_value = 5 if self.icon is not None else 15
|
||||||
icon_label.grid(
|
icon_label.grid(
|
||||||
@@ -183,118 +83,280 @@ class MessageDialog:
|
|||||||
font=font if font else ("Helvetica", 12),
|
font=font if font else ("Helvetica", 12),
|
||||||
pady=20,
|
pady=20,
|
||||||
)
|
)
|
||||||
text_label.grid(
|
text_label.grid(row=0, column=1, padx=(10, 20), sticky="nsew")
|
||||||
row=0,
|
|
||||||
column=1,
|
|
||||||
padx=(10, 20),
|
|
||||||
sticky="nsew",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create button frame
|
|
||||||
self.button_frame = ttk.Frame(frame)
|
self.button_frame = ttk.Frame(frame)
|
||||||
self.button_frame.grid(row=1, columnspan=2, pady=(8, 10))
|
self.button_frame.grid(row=1, columnspan=2, pady=(8, 10))
|
||||||
|
|
||||||
for i, btn_text in enumerate(buttons):
|
for i, btn_text in enumerate(buttons):
|
||||||
if commands and len(commands) > i and commands[i] is not None:
|
if commands and len(commands) > i and commands[i] is not None:
|
||||||
# Button with individual command
|
btn = ttk.Button(self.button_frame, text=btn_text, command=commands[i])
|
||||||
btn = ttk.Button(
|
|
||||||
self.button_frame,
|
|
||||||
text=btn_text,
|
|
||||||
command=commands[i],
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Default button set self.result and close window
|
btn = ttk.Button(self.button_frame, text=btn_text, command=lambda t=btn_text: self._on_button_click(t))
|
||||||
btn = ttk.Button(
|
|
||||||
self.button_frame,
|
|
||||||
text=btn_text,
|
|
||||||
command=lambda t=btn_text: self._on_button_click(t),
|
|
||||||
)
|
|
||||||
|
|
||||||
padx_value = 50 if self.icon is not None and len(
|
padx_value = 50 if self.icon is not None and len(buttons) == 2 else 10
|
||||||
buttons) == 2 else 10
|
btn.pack(side="left" if i == 0 else "right", padx=padx_value, pady=5)
|
||||||
btn.pack(side="left" if i == 0 else "right",
|
if i == 0: btn.focus_set()
|
||||||
padx=padx_value, pady=5)
|
|
||||||
btn.focus_set() if i == 0 else None # Set focus on first button
|
|
||||||
self.buttons_widgets.append(btn)
|
self.buttons_widgets.append(btn)
|
||||||
|
|
||||||
self.window.bind("<Return>", lambda event: self._on_enter_pressed())
|
self.window.bind("<Return>", lambda event: self._on_enter_pressed())
|
||||||
self.window.bind("<Left>", lambda event: self._navigate_left())
|
self.window.bind("<Left>", lambda event: self._navigate_left())
|
||||||
self.window.bind("<Right>", lambda event: self._navigate_right())
|
self.window.bind("<Right>", lambda event: self._navigate_right())
|
||||||
self.window.update_idletasks()
|
self.window.update_idletasks()
|
||||||
self.window.attributes("-alpha", 0.0) # 100% Transparencence
|
self.window.grab_set()
|
||||||
|
self.window.attributes("-alpha", 0.0)
|
||||||
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
|
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
|
||||||
self.window.update() # Window update before centering!
|
self.window.update()
|
||||||
LxTools.center_window_cross_platform(
|
LxTools.center_window_cross_platform(self.window, self.window.winfo_width(), self.window.winfo_height())
|
||||||
self.window, self.window.winfo_width(), self.window.winfo_height()
|
self.window.protocol("WM_DELETE_WINDOW", lambda: self._on_button_click("Cancel"))
|
||||||
)
|
|
||||||
|
|
||||||
# Close Window on Cancel
|
|
||||||
self.window.protocol(
|
|
||||||
"WM_DELETE_WINDOW", lambda: self._on_button_click("Cancel")
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_title(self) -> str:
|
def _get_title(self) -> str:
|
||||||
return {
|
return {"error": "Error", "info": "Info", "ask": "Question", "warning": "Warning"}[self.message_type]
|
||||||
"error": "Error",
|
|
||||||
"info": "Info",
|
|
||||||
"ask": "Question",
|
|
||||||
"warning": "Warning",
|
|
||||||
}[self.message_type]
|
|
||||||
|
|
||||||
def _navigate_left(self):
|
def _navigate_left(self):
|
||||||
if not self.buttons_widgets:
|
if not self.buttons_widgets: return
|
||||||
return
|
self.current_button_index = (self.current_button_index - 1) % len(self.buttons_widgets)
|
||||||
self.current_button_index = (self.current_button_index - 1) % len(
|
|
||||||
self.buttons_widgets
|
|
||||||
)
|
|
||||||
self.buttons_widgets[self.current_button_index].focus_set()
|
self.buttons_widgets[self.current_button_index].focus_set()
|
||||||
|
|
||||||
def _navigate_right(self):
|
def _navigate_right(self):
|
||||||
if not self.buttons_widgets:
|
if not self.buttons_widgets: return
|
||||||
return
|
self.current_button_index = (self.current_button_index + 1) % len(self.buttons_widgets)
|
||||||
self.current_button_index = (self.current_button_index + 1) % len(
|
|
||||||
self.buttons_widgets
|
|
||||||
)
|
|
||||||
self.buttons_widgets[self.current_button_index].focus_set()
|
self.buttons_widgets[self.current_button_index].focus_set()
|
||||||
|
|
||||||
def _on_enter_pressed(self):
|
def _on_enter_pressed(self):
|
||||||
focused = self.window.focus_get()
|
focused = self.window.focus_get()
|
||||||
if isinstance(focused, ttk.Button):
|
if isinstance(focused, ttk.Button): focused.invoke()
|
||||||
focused.invoke()
|
|
||||||
|
|
||||||
def _on_button_click(self, button_text: str) -> None:
|
def _on_button_click(self, button_text: str) -> None:
|
||||||
"""
|
if len(self.buttons) >= 3 and button_text.lower() in ["cancel", "abort", "exit"]:
|
||||||
Sets `self.result` based on the clicked button.
|
|
||||||
- Returns `None` if the button is "Cancel", "Abort", or "Exit" **and** there are 3 or more buttons.
|
|
||||||
- Returns `True` if the button is "Yes", "Ok", "Continue", "Next", or "Start".
|
|
||||||
- Returns `False` in all other cases (e.g., "No", closing with X, or fewer than 3 buttons).
|
|
||||||
"""
|
|
||||||
# Check: If there are 3+ buttons and the button text matches "Cancel", "Abort", or "Exit"
|
|
||||||
if len(self.buttons) >= 3 and button_text.lower() in [
|
|
||||||
"cancel",
|
|
||||||
"abort",
|
|
||||||
"exit",
|
|
||||||
]:
|
|
||||||
self.result = None
|
self.result = None
|
||||||
# Check: Button text is "Yes", "Ok", "Continue", "Next", or "Start"
|
elif button_text.lower() in ["yes", "ok", "continue", "next", "start", "select"]:
|
||||||
elif button_text.lower() in ["yes", "ok", "continue", "next", "start"]:
|
|
||||||
self.result = True
|
self.result = True
|
||||||
else:
|
else:
|
||||||
# Fallback for all other cases (e.g., "No", closing with X, or fewer than 3 buttons)
|
|
||||||
self.result = False
|
self.result = False
|
||||||
|
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
|
|
||||||
def show(self) -> Optional[bool]:
|
def show(self) -> Optional[bool]:
|
||||||
"""
|
self.window.wait_window()
|
||||||
Displays the dialog window and waits for user interaction.
|
return self.result
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool or None:
|
class CredentialsDialog:
|
||||||
- `True` if "Yes", "Ok", etc. was clicked.
|
"""
|
||||||
- `False` if "No" was clicked, or the window was closed with X (when there are 2 buttons).
|
A dialog for securely entering SSH/SFTP credentials.
|
||||||
- `None` if "Cancel", "Abort", or "Exit" was clicked **and** there are 3+ buttons,
|
"""
|
||||||
or the window was closed with X (when there are 3+ buttons).
|
def __init__(self, master: Optional[tk.Tk] = None, title: str = "SFTP Connection"):
|
||||||
"""
|
self.master = master
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
self.window = tk.Toplevel(master)
|
||||||
|
self.window.title(title)
|
||||||
|
self.window.resizable(False, False)
|
||||||
|
|
||||||
|
style = ttk.Style(self.window)
|
||||||
|
style.configure("Creds.TEntry", padding=(5, 2))
|
||||||
|
|
||||||
|
frame = ttk.Frame(self.window, padding=15)
|
||||||
|
frame.pack(expand=True, fill="both")
|
||||||
|
frame.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Host
|
||||||
|
ttk.Label(frame, text="Host:").grid(row=0, column=0, sticky="w", pady=2)
|
||||||
|
self.host_entry = ttk.Entry(frame, width=40, style="Creds.TEntry")
|
||||||
|
self.host_entry.grid(row=0, column=1, sticky="ew", pady=2)
|
||||||
|
|
||||||
|
# Port
|
||||||
|
ttk.Label(frame, text="Port:").grid(row=1, column=0, sticky="w", pady=2)
|
||||||
|
self.port_entry = ttk.Entry(frame, width=10, style="Creds.TEntry")
|
||||||
|
self.port_entry.insert(0, "22")
|
||||||
|
self.port_entry.grid(row=1, column=1, sticky="w", pady=2)
|
||||||
|
|
||||||
|
# Username
|
||||||
|
ttk.Label(frame, text="Username:").grid(row=2, column=0, sticky="w", pady=2)
|
||||||
|
self.username_entry = ttk.Entry(frame, width=40, style="Creds.TEntry")
|
||||||
|
self.username_entry.grid(row=2, column=1, sticky="ew", pady=2)
|
||||||
|
|
||||||
|
# Initial Path
|
||||||
|
ttk.Label(frame, text="Initial Path:").grid(row=3, column=0, sticky="w", pady=2)
|
||||||
|
self.path_entry = ttk.Entry(frame, width=40, style="Creds.TEntry")
|
||||||
|
self.path_entry.insert(0, "~")
|
||||||
|
self.path_entry.grid(row=3, column=1, sticky="ew", pady=2)
|
||||||
|
|
||||||
|
# Auth Method
|
||||||
|
ttk.Label(frame, text="Auth Method:").grid(row=4, column=0, sticky="w", pady=5)
|
||||||
|
auth_frame = ttk.Frame(frame)
|
||||||
|
auth_frame.grid(row=4, column=1, sticky="w", pady=2)
|
||||||
|
self.auth_method = tk.StringVar(value="password")
|
||||||
|
ttk.Radiobutton(auth_frame, text="Password", variable=self.auth_method, value="password", command=self._toggle_auth_fields).pack(side="left")
|
||||||
|
ttk.Radiobutton(auth_frame, text="Key File", variable=self.auth_method, value="keyfile", command=self._toggle_auth_fields).pack(side="left", padx=10)
|
||||||
|
|
||||||
|
# Password
|
||||||
|
self.password_label = ttk.Label(frame, text="Password:")
|
||||||
|
self.password_label.grid(row=5, column=0, sticky="w", pady=2)
|
||||||
|
self.password_entry = ttk.Entry(frame, show="*", width=40, style="Creds.TEntry")
|
||||||
|
self.password_entry.grid(row=5, column=1, sticky="ew", pady=2)
|
||||||
|
|
||||||
|
# Key File
|
||||||
|
self.keyfile_label = ttk.Label(frame, text="Key File:")
|
||||||
|
self.keyfile_label.grid(row=6, column=0, sticky="w", pady=2)
|
||||||
|
|
||||||
|
key_frame = ttk.Frame(frame)
|
||||||
|
key_frame.grid(row=6, column=1, sticky="ew", pady=2)
|
||||||
|
self.keyfile_entry = ttk.Entry(key_frame, width=36, style="Creds.TEntry")
|
||||||
|
self.keyfile_entry.pack(side="left", fill="x", expand=True)
|
||||||
|
self.keyfile_button = ttk.Button(key_frame, text="▼", width=2, command=self._show_key_menu)
|
||||||
|
self.keyfile_button.pack(side="left", padx=(2,0))
|
||||||
|
|
||||||
|
# Passphrase
|
||||||
|
self.passphrase_label = ttk.Label(frame, text="Passphrase:")
|
||||||
|
self.passphrase_label.grid(row=7, column=0, sticky="w", pady=2)
|
||||||
|
self.passphrase_entry = ttk.Entry(frame, show="*", width=40, style="Creds.TEntry")
|
||||||
|
self.passphrase_entry.grid(row=7, column=1, sticky="ew", pady=2)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_frame = ttk.Frame(frame)
|
||||||
|
button_frame.grid(row=8, column=1, sticky="e", pady=(15, 0))
|
||||||
|
connect_button = ttk.Button(button_frame, text="Connect", command=self._on_connect)
|
||||||
|
connect_button.pack(side="left", padx=5)
|
||||||
|
cancel_button = ttk.Button(button_frame, text="Cancel", command=self._on_cancel)
|
||||||
|
cancel_button.pack(side="left")
|
||||||
|
|
||||||
|
self._toggle_auth_fields()
|
||||||
|
self.window.bind("<Return>", lambda event: self._on_connect())
|
||||||
|
self.window.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||||
|
|
||||||
|
self.window.update_idletasks()
|
||||||
|
self.window.grab_set()
|
||||||
|
self.window.attributes("-alpha", 0.0)
|
||||||
|
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
|
||||||
|
self.window.update()
|
||||||
|
LxTools.center_window_cross_platform(self.window, self.window.winfo_width(), self.window.winfo_height())
|
||||||
|
self.host_entry.focus_set()
|
||||||
|
|
||||||
|
def _get_ssh_keys(self) -> List[str]:
|
||||||
|
ssh_path = os.path.expanduser("~/.ssh")
|
||||||
|
keys = []
|
||||||
|
if os.path.isdir(ssh_path):
|
||||||
|
try:
|
||||||
|
for item in os.listdir(ssh_path):
|
||||||
|
full_path = os.path.join(ssh_path, item)
|
||||||
|
if os.path.isfile(full_path) and not item.endswith('.pub') and 'known_hosts' not in item:
|
||||||
|
keys.append(full_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def _show_key_menu(self):
|
||||||
|
keys = self._get_ssh_keys()
|
||||||
|
if not keys:
|
||||||
|
return
|
||||||
|
|
||||||
|
menu = tk.Menu(self.window, tearoff=0)
|
||||||
|
for key_path in keys:
|
||||||
|
menu.add_command(label=key_path, command=lambda k=key_path: self._select_key_from_menu(k))
|
||||||
|
|
||||||
|
x = self.keyfile_button.winfo_rootx()
|
||||||
|
y = self.keyfile_button.winfo_rooty() + self.keyfile_button.winfo_height()
|
||||||
|
menu.tk_popup(x, y)
|
||||||
|
|
||||||
|
def _select_key_from_menu(self, key_path):
|
||||||
|
self.keyfile_entry.delete(0, tk.END)
|
||||||
|
self.keyfile_entry.insert(0, key_path)
|
||||||
|
|
||||||
|
def _toggle_auth_fields(self):
|
||||||
|
method = self.auth_method.get()
|
||||||
|
if method == "password":
|
||||||
|
self.password_label.grid()
|
||||||
|
self.password_entry.grid()
|
||||||
|
self.keyfile_label.grid_remove()
|
||||||
|
self.keyfile_entry.master.grid_remove()
|
||||||
|
self.passphrase_label.grid_remove()
|
||||||
|
self.passphrase_entry.grid_remove()
|
||||||
|
else:
|
||||||
|
self.password_label.grid_remove()
|
||||||
|
self.password_entry.grid_remove()
|
||||||
|
self.keyfile_label.grid()
|
||||||
|
self.keyfile_entry.master.grid()
|
||||||
|
self.passphrase_label.grid()
|
||||||
|
self.passphrase_entry.grid()
|
||||||
|
|
||||||
|
self.window.update_idletasks()
|
||||||
|
self.window.geometry("")
|
||||||
|
|
||||||
|
def _on_connect(self):
|
||||||
|
self.result = {
|
||||||
|
"host": self.host_entry.get(),
|
||||||
|
"port": int(self.port_entry.get() or 22),
|
||||||
|
"username": self.username_entry.get(),
|
||||||
|
"initial_path": self.path_entry.get() or "/",
|
||||||
|
"password": self.password_entry.get() if self.auth_method.get() == "password" else None,
|
||||||
|
"key_file": self.keyfile_entry.get() if self.auth_method.get() == "keyfile" else None,
|
||||||
|
"passphrase": self.passphrase_entry.get() if self.auth_method.get() == "keyfile" else None,
|
||||||
|
}
|
||||||
|
self.window.destroy()
|
||||||
|
|
||||||
|
def _on_cancel(self):
|
||||||
|
self.result = None
|
||||||
|
self.window.destroy()
|
||||||
|
|
||||||
|
def show(self) -> Optional[dict]:
|
||||||
|
self.window.wait_window()
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
class InputDialog:
|
||||||
|
"""
|
||||||
|
A simple dialog for getting a single line of text input from the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent, title: str, prompt: str, initial_value: str = ""):
|
||||||
|
self.result = None
|
||||||
|
self.window = tk.Toplevel(parent)
|
||||||
|
self.window.title(title)
|
||||||
|
self.window.transient(parent)
|
||||||
|
self.window.resizable(False, False)
|
||||||
|
|
||||||
|
frame = ttk.Frame(self.window, padding=15)
|
||||||
|
frame.pack(expand=True, fill="both")
|
||||||
|
|
||||||
|
ttk.Label(frame, text=prompt, wraplength=250).pack(pady=(0, 10))
|
||||||
|
|
||||||
|
self.entry = ttk.Entry(frame, width=40)
|
||||||
|
self.entry.insert(0, initial_value)
|
||||||
|
self.entry.pack(pady=5)
|
||||||
|
self.entry.focus_set()
|
||||||
|
self.entry.selection_range(0, tk.END)
|
||||||
|
|
||||||
|
button_frame = ttk.Frame(frame)
|
||||||
|
button_frame.pack(pady=(10, 0))
|
||||||
|
|
||||||
|
ok_button = ttk.Button(button_frame, text="OK", command=self._on_ok)
|
||||||
|
ok_button.pack(side="left", padx=5)
|
||||||
|
cancel_button = ttk.Button(
|
||||||
|
button_frame, text="Cancel", command=self._on_cancel)
|
||||||
|
cancel_button.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
self.window.bind("<Return>", lambda e: self._on_ok())
|
||||||
|
self.window.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||||
|
|
||||||
|
self.window.update_idletasks()
|
||||||
|
self.window.grab_set()
|
||||||
|
self.window.attributes("-alpha", 0.0)
|
||||||
|
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
|
||||||
|
self.window.update()
|
||||||
|
LxTools.center_window_cross_platform(
|
||||||
|
self.window, self.window.winfo_width(), self.window.winfo_height()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_ok(self):
|
||||||
|
self.result = self.entry.get()
|
||||||
|
if self.result:
|
||||||
|
self.window.destroy()
|
||||||
|
|
||||||
|
def _on_cancel(self):
|
||||||
|
self.result = None
|
||||||
|
self.window.destroy()
|
||||||
|
|
||||||
|
def show(self) -> Optional[str]:
|
||||||
self.window.wait_window()
|
self.window.wait_window()
|
||||||
return self.result
|
return self.result
|
||||||
|
Reference in New Issue
Block a user