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,454 @@
1
+ #!/usr/bin/env python
2
+ """Mini file dialog — intentionally lightweight.
3
+
4
+ Does NOT support preview_img, video_preview, or tool_tip.
5
+ Keeps a simple Treeview UI for speed and low memory.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import List, Optional
11
+
12
+ import customtkinter as ctk
13
+ import tkinter as tk
14
+ from CTkMessagebox import CTkMessagebox
15
+ from _tkinter import TclError
16
+ from tkinter import ttk
17
+
18
+ from ..core.filesystem import prompt_create_folder
19
+ from ..resources.icons import load_mini_icons
20
+ from ..system.platform import System
21
+
22
+
23
+ class MiniDialog:
24
+ """Compact file dialog (Mini style).
25
+
26
+ Public attribute contract (used by ``_functions``):
27
+ selected_path, selected_paths, selected_item, selected_items
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ method: str,
33
+ hidden: bool = False,
34
+ filetypes: Optional[List[str]] = None,
35
+ autocomplete: bool = False,
36
+ initial_dir: str = ".",
37
+ _extra_method: str = "",
38
+ foldercreation: bool = True,
39
+ geometry: str = "500x400",
40
+ title: str = "CTkFileDialog",
41
+ ):
42
+ self.master = ctk.CTkToplevel()
43
+ self.master.geometry(geometry_string=geometry)
44
+ self.master.title(title)
45
+ self._extra_method = _extra_method
46
+ self.foldercreation = foldercreation
47
+ self.tab_index = -1
48
+ self.method = method
49
+ self.hidden = hidden
50
+ self.filetypes = filetypes
51
+ self.autocomplete = autocomplete
52
+ self.initial_dir = initial_dir
53
+
54
+ if not self.initial_dir:
55
+ self.initial_dir = os.getcwd()
56
+ else:
57
+ self.initial_dir = System.get_path(path=self.initial_dir)
58
+
59
+ self.selected_path = ""
60
+ self.selected_paths: list = []
61
+ self.selected_items: list = []
62
+ self.selected_item = ""
63
+ self.files = {"name": [], "path": []}
64
+ self.absolute_paths: list = []
65
+ self.max_index = 0
66
+ self.filtered_paths: list = []
67
+
68
+ self.folder_image, self.file_image = load_mini_icons()
69
+
70
+ self._build_top()
71
+ self._build_center()
72
+ self.list_files()
73
+ self.master.bind_all("<Alt-Left>", lambda _: self._up())
74
+ self.master.wait_visibility()
75
+ self.master.grab_set()
76
+ self.master.wait_window()
77
+
78
+ # ------------------------------------------------------------------
79
+ # Helpers
80
+ # ------------------------------------------------------------------
81
+
82
+ def _get_path(self) -> str:
83
+ return os.path.abspath(
84
+ os.path.expandvars(os.path.expanduser(self.initial_dir))
85
+ )
86
+
87
+ def _create_new_folder(self) -> None:
88
+ if not self.foldercreation:
89
+ return
90
+ prompt_create_folder(self.initial_dir, on_success=self.list_files)
91
+
92
+ def update_entry(self, path) -> None:
93
+ self.path_entry.configure(state="normal")
94
+ self.path_entry.delete(0, ctk.END)
95
+ self.path_entry.insert(0, path)
96
+
97
+ # ------------------------------------------------------------------
98
+ # UI
99
+ # ------------------------------------------------------------------
100
+
101
+ def _build_top(self) -> None:
102
+ self.frame = ctk.CTkFrame(self.master)
103
+ self.frame.pack(fill=ctk.BOTH, expand=True)
104
+
105
+ self.path_frame = ctk.CTkFrame(self.frame)
106
+ self.path_frame.pack(fill=ctk.X, padx=10, pady=10)
107
+
108
+ self.path_entry = ctk.CTkEntry(self.path_frame)
109
+ self.path_entry.pack(expand=True, fill=ctk.X, side=ctk.LEFT, padx=10, pady=10)
110
+ self.path_entry.bind("<Return>", lambda _: self._on_enter_path())
111
+ self.path_entry.insert(0, self._get_path())
112
+
113
+ if self.autocomplete:
114
+ for bind in ("<Tab>", "<Down>", "<Up>"):
115
+ self.path_entry.bind(bind, self._autocomplete)
116
+
117
+ ctk.CTkButton(
118
+ self.path_frame, text="↑", width=30, command=self._up
119
+ ).pack(side=ctk.RIGHT, padx=10, pady=10)
120
+
121
+ if self.foldercreation:
122
+ ctk.CTkButton(
123
+ self.path_frame, text="+", width=30, command=self._create_new_folder
124
+ ).pack(side=ctk.RIGHT, padx=(0, 10), pady=10)
125
+
126
+ search_frame = ctk.CTkFrame(self.frame)
127
+ search_frame.pack(fill=ctk.X, padx=10, pady=(0, 10))
128
+ ctk.CTkLabel(search_frame, text="Search:", font=("Arial", 12)).pack(
129
+ side=ctk.LEFT, padx=(0, 10)
130
+ )
131
+ self.search_entry = ctk.CTkEntry(
132
+ search_frame, placeholder_text="Type to search files..."
133
+ )
134
+ self.search_entry.pack(expand=True, fill=ctk.X, side=ctk.LEFT)
135
+ self.search_entry.bind("<KeyRelease>", lambda _: self._search_files())
136
+
137
+ btn_frame = ctk.CTkFrame(self.frame, fg_color="transparent")
138
+ btn_frame.pack(side=ctk.BOTTOM, fill=ctk.X, padx=10, pady=10)
139
+ ctk.CTkButton(btn_frame, text="OK", command=self._on_select).pack(
140
+ side=ctk.RIGHT
141
+ )
142
+ ctk.CTkButton(btn_frame, text="Cancel", command=self._on_cancel).pack(
143
+ side=ctk.RIGHT, padx=10
144
+ )
145
+
146
+ def _build_center(self) -> None:
147
+ self.tree_frame = ctk.CTkFrame(self.frame)
148
+ self.tree_frame.pack(fill=ctk.BOTH, expand=True, padx=10, pady=5)
149
+
150
+ style = ttk.Style()
151
+ style.theme_use("clam")
152
+ mode = ctk.get_appearance_mode()
153
+ if mode == "Dark":
154
+ style.configure(
155
+ "Treeview",
156
+ background="#242424",
157
+ foreground="#FFFFFF",
158
+ fieldbackground="#242424",
159
+ bordercolor="#242424",
160
+ rowheight=30,
161
+ )
162
+ style.map(
163
+ "Treeview",
164
+ background=[("selected", "#444444")],
165
+ foreground=[("selected", "#FFFFFF")],
166
+ )
167
+ else:
168
+ style.configure(
169
+ "Treeview",
170
+ background="#FFFFFF",
171
+ foreground="#000000",
172
+ fieldbackground="#FFFFFF",
173
+ bordercolor="#DDDDDD",
174
+ rowheight=30,
175
+ )
176
+ style.map(
177
+ "Treeview",
178
+ background=[("selected", "#E0E0E0")],
179
+ foreground=[("selected", "#000000")],
180
+ )
181
+
182
+ multi = self.method in (
183
+ "askopenfilenames",
184
+ "askopenfiles",
185
+ "askdirectories",
186
+ "askopenpathnames",
187
+ )
188
+ self.tree = ttk.Treeview(
189
+ self.tree_frame,
190
+ show="tree",
191
+ selectmode="extended" if multi else "browse",
192
+ )
193
+ self.tree.bind("<Double-1>", self._on_click)
194
+ self.tree.bind("<Button-1>", self._on_select_item)
195
+ self.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
196
+
197
+ # ------------------------------------------------------------------
198
+ # Listing / search
199
+ # ------------------------------------------------------------------
200
+
201
+ def list_files(self) -> None:
202
+ path = os.path.abspath(
203
+ os.path.expanduser(os.path.expandvars(self.path_entry.get()))
204
+ )
205
+ if os.path.isfile(path):
206
+ return
207
+ if not os.path.exists(path):
208
+ return
209
+ try:
210
+ try:
211
+ for item in self.tree.get_children():
212
+ self.tree.delete(item)
213
+ except TclError:
214
+ return
215
+
216
+ self.files = {"name": [], "path": []}
217
+ filtered = []
218
+
219
+ for f in os.scandir(path):
220
+ if (
221
+ (
222
+ f.is_dir()
223
+ or (
224
+ self.method not in ("askdirectory", "askdirectories")
225
+ and f.is_file()
226
+ )
227
+ )
228
+ and (self.hidden or not f.name.startswith("."))
229
+ and (
230
+ f.is_dir()
231
+ or not self.filetypes
232
+ or any(f.name.endswith(ext) for ext in self.filetypes)
233
+ )
234
+ ):
235
+ filtered.append(f)
236
+ self.files["name"].append(f.name)
237
+ self.files["path"].append(f.path)
238
+
239
+ sorted_files = sorted(
240
+ filtered, key=lambda f: (not f.is_dir(), f.name.lower())
241
+ )
242
+ self.update_entry(path=path)
243
+
244
+ for f in sorted_files:
245
+ icon = self.folder_image if f.is_dir() else self.file_image
246
+ self.tree.insert("", tk.END, text=f.name, image=icon)
247
+
248
+ self.absolute_paths = [f.path for f in sorted_files]
249
+
250
+ except PermissionError:
251
+ CTkMessagebox(
252
+ message="Permission Denied!", title="Error", icon="cancel"
253
+ )
254
+ self._on_cancel(destroy=False)
255
+ else:
256
+ self.max_index = len(self.files["name"])
257
+
258
+ def _search_files(self) -> None:
259
+ if not hasattr(self, "files") or not self.files["name"]:
260
+ return
261
+ query = self.search_entry.get().lower()
262
+ for item in self.tree.get_children():
263
+ self.tree.delete(item)
264
+
265
+ if not query:
266
+ self.list_files()
267
+ return
268
+
269
+ self.filtered_paths = []
270
+ for name, path in zip(self.files["name"], self.files["path"]):
271
+ if query in name.lower():
272
+ is_dir = os.path.isdir(path)
273
+ icon = self.folder_image if is_dir else self.file_image
274
+ self.tree.insert("", tk.END, text=name, image=icon)
275
+ self.filtered_paths.append(path)
276
+ self.absolute_paths = self.filtered_paths
277
+
278
+ # ------------------------------------------------------------------
279
+ # Selection / navigation
280
+ # ------------------------------------------------------------------
281
+
282
+ def _autocomplete(self, event: tk.Event) -> str:
283
+ if not self.files["name"] or not hasattr(self, "max_index"):
284
+ return "break"
285
+ if event.keysym == "Up":
286
+ self.tab_index = (self.tab_index - 1) % self.max_index
287
+ else:
288
+ self.tab_index = (self.tab_index + 1) % self.max_index
289
+
290
+ path = self.absolute_paths[self.tab_index]
291
+ self.path_entry.delete(0, ctk.END)
292
+ self.path_entry.insert(0, path)
293
+ item_id = self.tree.get_children()[self.tab_index]
294
+ self.tree.focus(item_id)
295
+ self.tree.selection_set(item_id)
296
+ self.tree.see(item_id)
297
+ self.selected_item = path
298
+ return "break"
299
+
300
+ def _on_enter_path(self) -> None:
301
+ path = os.path.abspath(
302
+ os.path.expanduser(os.path.expandvars(self.path_entry.get()))
303
+ )
304
+ if os.path.isdir(path):
305
+ self.initial_dir = path
306
+ self.list_files()
307
+ else:
308
+ if os.path.isfile(path):
309
+ return
310
+ self.path_entry.configure(state="normal")
311
+ if not os.path.exists(path=path):
312
+ self._on_cancel(destroy=False)
313
+ self.update_entry(path=self.initial_dir)
314
+ CTkMessagebox(
315
+ title="Error",
316
+ icon="cancel",
317
+ message="No such file or directory!",
318
+ )
319
+
320
+ def _on_cancel(self, destroy: bool = True) -> None:
321
+ self.selected_path = None
322
+ self.selected_item = None
323
+ self.selected_paths = None
324
+ self.selected_items = None
325
+ if destroy:
326
+ self.master.destroy()
327
+
328
+ def _on_select(self) -> None:
329
+ path = self.path_entry.get().strip() if hasattr(self, "path_entry") else ""
330
+ if path:
331
+ path = os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
332
+ if not os.path.dirname(path):
333
+ path = os.path.join(self.initial_dir, path)
334
+
335
+ if self.method in ("asksaveasfile", "asksaveasfilename"):
336
+ if not path or os.path.isdir(path):
337
+ return
338
+ if os.path.exists(path) and self._extra_method != "askopenfile":
339
+ opts = CTkMessagebox(
340
+ message="This file already exists! Do you want to overwrite it?",
341
+ title="Error",
342
+ icon="warning",
343
+ option_1="Yes",
344
+ option_2="No",
345
+ )
346
+ if opts.get() == "No":
347
+ return
348
+ self.selected_path = path
349
+ self.master.destroy()
350
+ return
351
+
352
+ if self.method in ("askopenfiles", "askopenfilenames"):
353
+ selected_items = self.tree.selection()
354
+ selected_paths = [
355
+ self.absolute_paths[self.tree.index(item)]
356
+ for item in selected_items
357
+ if os.path.isfile(self.absolute_paths[self.tree.index(item)])
358
+ ]
359
+ if selected_paths:
360
+ self.selected_paths = selected_paths
361
+ self.master.destroy()
362
+ return
363
+
364
+ if self.method == "askdirectories":
365
+ selected_items = self.tree.selection()
366
+ selected_paths = [
367
+ self.absolute_paths[self.tree.index(item)]
368
+ for item in selected_items
369
+ if os.path.isdir(self.absolute_paths[self.tree.index(item)])
370
+ ]
371
+ if selected_paths:
372
+ self.selected_paths = selected_paths
373
+ self.master.destroy()
374
+ return
375
+
376
+ if self.method == "askopenpathnames":
377
+ selected_items = self.tree.selection()
378
+ selected_paths = [
379
+ self.absolute_paths[self.tree.index(item)] for item in selected_items
380
+ ]
381
+ if selected_paths:
382
+ self.selected_paths = selected_paths
383
+ self.master.destroy()
384
+ return
385
+
386
+ if self.method in (
387
+ "askopenfilename",
388
+ "askopenfile",
389
+ "askdirectory",
390
+ "askopenpathname",
391
+ ):
392
+ if not self.selected_item:
393
+ return
394
+ if self.method == "askdirectory" and os.path.isdir(self.selected_item):
395
+ self.selected_path = self.selected_item
396
+ self.master.destroy()
397
+ return
398
+ if self.method in ("askopenfilename", "askopenfile") and os.path.isfile(
399
+ self.selected_item
400
+ ):
401
+ self.selected_path = self.selected_item
402
+ self.master.destroy()
403
+ return
404
+ if self.method == "askopenpathname":
405
+ self.selected_path = self.selected_item
406
+ self.master.destroy()
407
+ return
408
+
409
+ def _on_select_item(self, event=None) -> None:
410
+ selected_item = self.tree.focus()
411
+ items = self.tree.get_children()
412
+ if not selected_item or not items:
413
+ return
414
+ try:
415
+ idx = items.index(selected_item)
416
+ if idx < len(self.absolute_paths):
417
+ self.selected_item = self.absolute_paths[idx]
418
+ except (ValueError, IndexError):
419
+ pass
420
+
421
+ def _on_click(self, event=None) -> None:
422
+ selected_item = self.tree.focus()
423
+ items = self.tree.get_children()
424
+ if not selected_item:
425
+ return
426
+ try:
427
+ idx = items.index(selected_item)
428
+ if idx >= len(self.absolute_paths):
429
+ return
430
+ self.selected_item = self.absolute_paths[idx]
431
+ except (ValueError, IndexError):
432
+ return
433
+
434
+ if os.path.isdir(self.selected_item):
435
+ self.initial_dir = self.selected_item
436
+ self.path_entry.delete(0, ctk.END)
437
+ self.path_entry.insert(0, self.selected_item)
438
+ self.list_files()
439
+ return
440
+ self.path_entry.delete(0, ctk.END)
441
+ self.path_entry.insert(0, self.selected_item)
442
+
443
+ def _up(self) -> None:
444
+ current_path = os.path.abspath(
445
+ os.path.expandvars(os.path.expanduser(self.initial_dir))
446
+ )
447
+ self.initial_dir = os.path.dirname(current_path)
448
+ self.path_entry.delete(0, ctk.END)
449
+ self.path_entry.insert(0, self.initial_dir)
450
+ self.list_files()
451
+
452
+
453
+ # Backward-compatible alias
454
+ _MiniDialog = MiniDialog
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python
2
+ """Custom tooltip that tolerates destroyed widgets."""
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ import _tkinter
8
+ from CTkToolTip import CTkToolTip
9
+
10
+
11
+ class CustomToolTip(CTkToolTip):
12
+ """CTkToolTip subclass that safely hides when the parent is gone."""
13
+
14
+ def _show(self) -> None:
15
+ if not self.widget.winfo_exists():
16
+ self.hide()
17
+ self.destroy()
18
+ return
19
+
20
+ if self.status == "inside" and time.time() - self.last_moved >= self.delay:
21
+ self.status = "visible"
22
+ try:
23
+ self.deiconify()
24
+ except _tkinter.TclError:
25
+ pass
@@ -0,0 +1,9 @@
1
+ """Utility helpers."""
2
+ from .helpers import apply_defaultext, fix_name, format_size, normalize_filetypes
3
+
4
+ __all__ = [
5
+ "normalize_filetypes",
6
+ "apply_defaultext",
7
+ "fix_name",
8
+ "format_size",
9
+ ]
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python
2
+ """Shared pure helpers (no UI, no FS side-effects beyond path math)."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import List, Optional, Tuple, Union
7
+
8
+
9
+ def normalize_filetypes(
10
+ filetypes: Optional[List[Union[str, Tuple[str, str]]]],
11
+ ) -> Optional[List[str]]:
12
+ """Convert tkinter-style filetypes list into simplified extensions.
13
+
14
+ Accepts a list of strings or ``(label, pattern)`` tuples where pattern
15
+ may contain space-separated globs. ``"*"`` / ``"*.*"`` become ``""``
16
+ (match-all). Leading ``*`` characters are stripped so ``"*.py"``
17
+ becomes ``".py"``.
18
+ """
19
+ if not filetypes:
20
+ return None
21
+ normalized: list[str] = []
22
+ for entry in filetypes:
23
+ if isinstance(entry, (tuple, list)) and len(entry) >= 2:
24
+ patterns = str(entry[1]).split()
25
+ for pat in patterns:
26
+ pat = pat.strip()
27
+ if pat in ("*", "*.*"):
28
+ normalized.append("")
29
+ else:
30
+ normalized.append(pat.lstrip("*"))
31
+ else:
32
+ normalized.append(str(entry))
33
+ return normalized
34
+
35
+
36
+ def apply_defaultext(path: Optional[str], defaultext: Optional[str]) -> Optional[str]:
37
+ """Append *defaultext* to *path* if it has no extension yet."""
38
+ if not path or not defaultext:
39
+ return path
40
+ _, ext = os.path.splitext(path)
41
+ if ext:
42
+ return path
43
+ if not defaultext.startswith("."):
44
+ defaultext = "." + defaultext
45
+ return path + defaultext
46
+
47
+
48
+ def fix_name(name: str, max_len: int = 18) -> str:
49
+ """Truncate a display name for grid buttons."""
50
+ if len(name) > max_len:
51
+ return name[: max_len - 3]
52
+ return name
53
+
54
+
55
+ def format_size(size: int) -> str:
56
+ """Human-readable file size."""
57
+ if size < 1024:
58
+ return f"{size} B"
59
+ if size < 1024 * 1024:
60
+ return f"{size / 1024:.1f} KB"
61
+ return f"{size / (1024 * 1024):.1f} MB"