CTkFileDialog-plus 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ """Core non-UI logic: filesystem, sorting, search."""
2
+ from .filesystem import create_folder, get_file_info, list_directory, prompt_create_folder
3
+ from .search import filter_by_query
4
+ from .sorting import sort_files
5
+
6
+ __all__ = [
7
+ "list_directory",
8
+ "create_folder",
9
+ "prompt_create_folder",
10
+ "get_file_info",
11
+ "sort_files",
12
+ "filter_by_query",
13
+ ]
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python
2
+ """Filesystem operations used by the dialogs."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from typing import Any, Callable, List, Optional
8
+
9
+ from CTkMessagebox import CTkMessagebox
10
+
11
+ from ..system.platform import find_owner
12
+
13
+
14
+ def list_directory(
15
+ path: str,
16
+ *,
17
+ method: str,
18
+ hidden: bool = False,
19
+ filetypes: Optional[List[str]] = None,
20
+ ) -> List[str]:
21
+ """Return names of entries in *path* filtered by dialog mode and options.
22
+
23
+ Directories are always included (unless hidden). Files are included
24
+ unless the method is a pure directory-picker, and must match *filetypes*
25
+ when provided.
26
+ """
27
+ dir_only = method in ("askdirectory", "askdirectories")
28
+ result: List[str] = []
29
+ try:
30
+ for entry in os.scandir(path):
31
+ if entry.name.startswith(".") and not hidden:
32
+ continue
33
+ if entry.is_dir():
34
+ result.append(entry.name)
35
+ elif not dir_only and entry.is_file():
36
+ if not filetypes or any(entry.name.endswith(ext) for ext in filetypes):
37
+ result.append(entry.name)
38
+ except (PermissionError, FileNotFoundError, OSError):
39
+ raise
40
+ return result
41
+
42
+
43
+ def create_folder(parent: str, name: str) -> Optional[str]:
44
+ """Create a new folder under *parent*.
45
+
46
+ Returns the new path on success, or ``None`` on failure (after showing
47
+ a message box). Raises nothing — errors are reported via UI.
48
+ """
49
+ name = (name or "").strip()
50
+ if not name:
51
+ return None
52
+ new_path = os.path.join(parent, name)
53
+ try:
54
+ os.makedirs(new_path, exist_ok=False)
55
+ return new_path
56
+ except FileExistsError:
57
+ CTkMessagebox(
58
+ message="A file or folder with that name already exists!",
59
+ title="Error",
60
+ icon="cancel",
61
+ )
62
+ except PermissionError:
63
+ CTkMessagebox(message="Permission denied!", title="Error", icon="cancel")
64
+ except OSError as e:
65
+ CTkMessagebox(
66
+ message=f"Could not create folder: {e}",
67
+ title="Error",
68
+ icon="cancel",
69
+ )
70
+ return None
71
+
72
+
73
+ def get_file_info(path: str) -> str:
74
+ """Return a multi-line tooltip string for *path*."""
75
+ try:
76
+ st = os.stat(path)
77
+ owner = find_owner(path)
78
+ fecha = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(st.st_ctime))
79
+ return (
80
+ f"File: {os.path.basename(path)}\n"
81
+ f" creation: {fecha}\n"
82
+ f" owner: {owner}\n"
83
+ f" path: {path}\n"
84
+ f" "
85
+ )
86
+ except Exception as e:
87
+ return f"Error getting info: {e}"
88
+
89
+
90
+ def prompt_create_folder(parent_path: str, on_success: Optional[Callable[[], Any]] = None) -> None:
91
+ """Show an input dialog, create the folder, then call *on_success*."""
92
+ import customtkinter as ctk
93
+
94
+ dialog = ctk.CTkInputDialog(text="Enter new folder name:", title="New Folder")
95
+ name = dialog.get_input()
96
+ if create_folder(parent_path, name or "") and on_success:
97
+ on_success()
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env python
2
+ """Search / filter helpers."""
3
+ from __future__ import annotations
4
+
5
+ from typing import List
6
+
7
+
8
+ def filter_by_query(files: List[str], query: str) -> List[str]:
9
+ """Case-insensitive substring filter on file names."""
10
+ q = (query or "").lower().strip()
11
+ if not q:
12
+ return list(files)
13
+ return [f for f in files if q in f.lower()]
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python
2
+ """File sorting helpers."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import List
7
+
8
+
9
+ def sort_files(files: List[str], current_path: str, sort_by: str = "name") -> List[str]:
10
+ """Sort *files* (names relative to *current_path*).
11
+
12
+ Directories always sort before files. *sort_by* may be one of
13
+ ``name``, ``date``, ``type``, ``size``, ``modified``.
14
+ """
15
+
16
+ def info(filename: str) -> dict:
17
+ full = os.path.join(current_path, filename)
18
+ try:
19
+ st = os.stat(full)
20
+ return {
21
+ "name": filename.lower(),
22
+ "date": st.st_mtime,
23
+ "modified": st.st_mtime,
24
+ "type": os.path.splitext(filename)[1].lower(),
25
+ "size": st.st_size,
26
+ "is_dir": os.path.isdir(full),
27
+ }
28
+ except OSError:
29
+ return {
30
+ "name": filename.lower(),
31
+ "date": 0,
32
+ "modified": 0,
33
+ "type": "",
34
+ "size": 0,
35
+ "is_dir": os.path.isdir(full),
36
+ }
37
+
38
+ return sorted(
39
+ files,
40
+ key=lambda f: (not info(f)["is_dir"], info(f).get(sort_by, 0)),
41
+ )
@@ -0,0 +1,4 @@
1
+ """Media preview helpers (image / video). Extensible for future formats."""
2
+ from .media import get_video_frame, is_image, is_video, thumbnail_image
3
+
4
+ __all__ = ["is_image", "is_video", "get_video_frame", "thumbnail_image"]
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python
2
+ """Image and video detection / frame extraction.
3
+
4
+ Architecture is ready for future PDF / audio preview plugins:
5
+ register additional checkers via the same pattern (is_* + extract).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Optional
11
+
12
+ from PIL import Image
13
+
14
+ _IMAGE_EXTENSIONS = frozenset(
15
+ {
16
+ ".bmp",
17
+ ".dib",
18
+ ".gif",
19
+ ".ico",
20
+ ".im",
21
+ ".jpg",
22
+ ".jpeg",
23
+ ".jpe",
24
+ ".pcx",
25
+ ".png",
26
+ ".ppm",
27
+ ".pbm",
28
+ ".pgm",
29
+ ".tif",
30
+ ".tiff",
31
+ ".webp",
32
+ }
33
+ )
34
+
35
+ _VIDEO_EXTENSIONS = frozenset(
36
+ {
37
+ ".avi",
38
+ ".mp4",
39
+ ".mov",
40
+ ".mkv",
41
+ ".webm",
42
+ ".flv",
43
+ ".wmv",
44
+ ".mpg",
45
+ ".mpeg",
46
+ ".3gp",
47
+ ".m4v",
48
+ }
49
+ )
50
+
51
+
52
+ def is_image(path: str) -> bool:
53
+ if not path or not os.path.isfile(path):
54
+ return False
55
+ if os.path.splitext(path)[1].lower() not in _IMAGE_EXTENSIONS:
56
+ return False
57
+ try:
58
+ with Image.open(path) as img:
59
+ img.verify()
60
+ return True
61
+ except Exception:
62
+ return False
63
+
64
+
65
+ def is_video(path: str) -> bool:
66
+ if not path or not os.path.isfile(path):
67
+ return False
68
+ if os.path.splitext(path)[1].lower() not in _VIDEO_EXTENSIONS:
69
+ return False
70
+ try:
71
+ import cv2
72
+
73
+ cap = cv2.VideoCapture(path)
74
+ valid = cap.isOpened()
75
+ cap.release()
76
+ return valid
77
+ except Exception:
78
+ return False
79
+
80
+
81
+ def get_video_frame(path: str, frame_number: int = 0) -> Optional[Image.Image]:
82
+ """Return a PIL Image of a single video frame, or None."""
83
+ if not path or not os.path.isfile(path):
84
+ return None
85
+
86
+ try:
87
+ import cv2
88
+ except ImportError:
89
+ return None
90
+
91
+ try:
92
+ cap = cv2.VideoCapture(path)
93
+ if not cap.isOpened():
94
+ return None
95
+
96
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
97
+ if total_frames > 0:
98
+ if frame_number < 0:
99
+ frame_number = max(total_frames // 2, 0)
100
+ elif frame_number >= total_frames:
101
+ frame_number = max(total_frames - 1, 0)
102
+ else:
103
+ frame_number = 0
104
+
105
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
106
+ ret, frame = cap.read()
107
+ cap.release()
108
+ if not ret or frame is None:
109
+ return None
110
+
111
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
112
+ return Image.fromarray(frame)
113
+ except Exception:
114
+ return None
115
+
116
+
117
+ def thumbnail_image(path: str, size: tuple[int, int] = (32, 32)) -> Optional[Image.Image]:
118
+ """Open an image and return a thumbnail, or None on failure."""
119
+ if not path or not os.path.isfile(path):
120
+ return None
121
+ try:
122
+ with Image.open(path) as img:
123
+ img.thumbnail(size, Image.Resampling.LANCZOS)
124
+ return img.copy()
125
+ except Exception:
126
+ return None
@@ -0,0 +1,14 @@
1
+ """Static resources (icons)."""
2
+ from .icons import (
3
+ EXTENSION_ICONS,
4
+ icon_for_extension,
5
+ load_default_icons,
6
+ load_mini_icons,
7
+ )
8
+
9
+ __all__ = [
10
+ "EXTENSION_ICONS",
11
+ "load_default_icons",
12
+ "load_mini_icons",
13
+ "icon_for_extension",
14
+ ]
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python
2
+ """Icon loading for Default and Mini dialogs."""
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Dict
7
+
8
+ import customtkinter as ctk
9
+ from PIL import Image
10
+ import tkinter as tk
11
+
12
+ # Package root (CTkFileDialog/)
13
+ _PACKAGE_DIR = Path(__file__).resolve().parent.parent
14
+ _ICON_DIR = _PACKAGE_DIR / "icons"
15
+ _MINI_ICON_DIR = _ICON_DIR / "_IconsMini"
16
+
17
+ # Extension → icon key mapping used by Default dialog
18
+ EXTENSION_ICONS: Dict[str, str] = {
19
+ ".webp": "webp",
20
+ ".awk": "bash",
21
+ ".mp4": "video",
22
+ ".mvk": "video",
23
+ ".sh": "bash",
24
+ ".zsh": "bash",
25
+ ".py": "python",
26
+ ".png": "image",
27
+ ".jpg": "image",
28
+ ".jpeg": "image",
29
+ ".txt": "text",
30
+ ".js": "javascript",
31
+ ".md": "markdown",
32
+ ".php": "php",
33
+ ".html": "html",
34
+ ".css": "css",
35
+ ".ini": "ini",
36
+ ".conf": "conf",
37
+ ".json": "json",
38
+ ".odt": "odt",
39
+ ".pdf": "pdf",
40
+ ".exe": "exe",
41
+ ".gz": "gz",
42
+ }
43
+
44
+ # Filename (without path) → logical name for Default CTkImage icons
45
+ _DEFAULT_ICON_FILES = {
46
+ "folder": "folder.png",
47
+ "bash": "bash.png",
48
+ "image": "image.png",
49
+ "python": "python.png",
50
+ "text": "text.png",
51
+ "markdown": "markdown.png",
52
+ "javascript": "javascript.png",
53
+ "php": "php.png",
54
+ "html": "html.png",
55
+ "css": "css.png",
56
+ "ini": "ini.png",
57
+ "conf": "conf.png",
58
+ "exe": "exe.png",
59
+ "odt": "odt.png",
60
+ "pdf": "pdf.png",
61
+ "json": "json.png",
62
+ "gz": "gz.png",
63
+ "video": "video.png",
64
+ "awk": "bash.png",
65
+ "webp": "image.png",
66
+ "default": "text.png",
67
+ }
68
+
69
+
70
+ def load_default_icons(size: tuple[int, int] = (40, 40)) -> Dict[str, ctk.CTkImage]:
71
+ """Load all Default-dialog CTkImage icons."""
72
+ icons: Dict[str, ctk.CTkImage] = {}
73
+ for key, filename in _DEFAULT_ICON_FILES.items():
74
+ path = _ICON_DIR / filename
75
+ icons[key] = ctk.CTkImage(Image.open(path), size=size)
76
+ return icons
77
+
78
+
79
+ def load_mini_icons() -> tuple[tk.PhotoImage, tk.PhotoImage]:
80
+ """Return (folder_image, file_image) for Mini dialog Treeview."""
81
+ folder = tk.PhotoImage(file=str(_MINI_ICON_DIR / "folder.png"))
82
+ file_img = tk.PhotoImage(file=str(_MINI_ICON_DIR / "file.png"))
83
+ return folder, file_img
84
+
85
+
86
+ def icon_for_extension(ext: str, icons: Dict[str, ctk.CTkImage]) -> ctk.CTkImage:
87
+ """Resolve a CTkImage for a file extension."""
88
+ key = EXTENSION_ICONS.get(ext.lower(), "default")
89
+ return icons.get(key, icons["default"])
@@ -0,0 +1,4 @@
1
+ """System / platform helpers."""
2
+ from .platform import System, find_owner
3
+
4
+ __all__ = ["System", "find_owner"]
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env python
2
+ """Platform-specific utilities (owner lookup, path helpers)."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ from pathlib import Path
8
+
9
+
10
+ def find_owner(path) -> str:
11
+ """Return the owner of *path* (Unix or Windows)."""
12
+ system = platform.system()
13
+ path = str(path)
14
+
15
+ if system == "Windows":
16
+ return _get_windows_owner(path)
17
+ return _get_unix_owner(path)
18
+
19
+
20
+ def _get_unix_owner(path: str) -> str:
21
+ try:
22
+ return Path(path).owner()
23
+ except Exception:
24
+ return "unknown:unknown"
25
+
26
+
27
+ def _get_windows_owner(path: str) -> str:
28
+ import ctypes
29
+ from ctypes import wintypes
30
+
31
+ GetNamedSecurityInfoW = ctypes.windll.advapi32.GetNamedSecurityInfoW
32
+ LookupAccountSidW = ctypes.windll.advapi32.LookupAccountSidW
33
+ LocalFree = ctypes.windll.kernel32.LocalFree
34
+
35
+ OWNER_SECURITY_INFORMATION = 0x00000001
36
+ SE_FILE_OBJECT = 1
37
+
38
+ pSidOwner = ctypes.c_void_p()
39
+ pSD = ctypes.c_void_p()
40
+
41
+ result = GetNamedSecurityInfoW(
42
+ ctypes.c_wchar_p(path),
43
+ SE_FILE_OBJECT,
44
+ OWNER_SECURITY_INFORMATION,
45
+ ctypes.byref(pSidOwner),
46
+ None, None, None,
47
+ ctypes.byref(pSD),
48
+ )
49
+
50
+ if result != 0:
51
+ return "unknown"
52
+
53
+ name = ctypes.create_unicode_buffer(256)
54
+ domain = ctypes.create_unicode_buffer(256)
55
+ name_size = wintypes.DWORD(len(name))
56
+ domain_size = wintypes.DWORD(len(domain))
57
+ sid_name_use = wintypes.DWORD()
58
+
59
+ success = LookupAccountSidW(
60
+ None,
61
+ pSidOwner,
62
+ name,
63
+ ctypes.byref(name_size),
64
+ domain,
65
+ ctypes.byref(domain_size),
66
+ ctypes.byref(sid_name_use),
67
+ )
68
+
69
+ LocalFree(pSD)
70
+
71
+ if not success:
72
+ return "unknown"
73
+
74
+ return f"{name.value}"
75
+
76
+
77
+ class System:
78
+ """Thin path helpers used by both Default and Mini dialogs."""
79
+
80
+ @staticmethod
81
+ def get_path(path=None) -> str:
82
+ if path is None:
83
+ path = os.getcwd()
84
+ return f"{path}" if path == os.getenv("HOME") else path
85
+
86
+ @staticmethod
87
+ def parse_path(path: str) -> str:
88
+ return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
@@ -0,0 +1,5 @@
1
+ """UI layer: Default and Mini dialogs."""
2
+ from .default_dialog import DrawApp, _DrawApp
3
+ from .mini_dialog import MiniDialog, _MiniDialog
4
+
5
+ __all__ = ["DrawApp", "_DrawApp", "MiniDialog", "_MiniDialog"]