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.
- CTkFileDialog/Constants/__init__.py +45 -0
- CTkFileDialog/Constants/constants.py +53 -0
- CTkFileDialog/Dialog.py +25 -0
- CTkFileDialog/__init__.py +36 -0
- CTkFileDialog/_functions.py +521 -0
- CTkFileDialog/_system.py +5 -0
- CTkFileDialog/core/__init__.py +13 -0
- CTkFileDialog/core/filesystem.py +97 -0
- CTkFileDialog/core/search.py +13 -0
- CTkFileDialog/core/sorting.py +41 -0
- CTkFileDialog/preview/__init__.py +4 -0
- CTkFileDialog/preview/media.py +126 -0
- CTkFileDialog/resources/__init__.py +14 -0
- CTkFileDialog/resources/icons.py +89 -0
- CTkFileDialog/system/__init__.py +4 -0
- CTkFileDialog/system/platform.py +88 -0
- CTkFileDialog/ui/__init__.py +5 -0
- CTkFileDialog/ui/default_dialog.py +1013 -0
- CTkFileDialog/ui/mini_dialog.py +454 -0
- CTkFileDialog/ui/tooltip.py +25 -0
- CTkFileDialog/utils/__init__.py +9 -0
- CTkFileDialog/utils/helpers.py +61 -0
- ctkfiledialog_plus-2.1.1.dist-info/METADATA +592 -0
- ctkfiledialog_plus-2.1.1.dist-info/RECORD +27 -0
- ctkfiledialog_plus-2.1.1.dist-info/WHEEL +5 -0
- ctkfiledialog_plus-2.1.1.dist-info/licenses/LICENSE +21 -0
- ctkfiledialog_plus-2.1.1.dist-info/top_level.txt +1 -0
|
@@ -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,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"
|