CTKFileDialog-plus 0.3.0__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,1409 @@
1
+ #!/usr/bin/env python
2
+ import os, re, cv2, time
3
+ import customtkinter as ctk
4
+ from CTkMessagebox import CTkMessagebox
5
+ from pathlib import Path
6
+ from PIL import Image
7
+ import tkinter as tk
8
+ from CTkToolTip import *
9
+ from typing import Any, Literal, Optional, TextIO, List
10
+ from _tkinter import TclError
11
+ from tkinter import ttk
12
+ import _tkinter
13
+ from ._system import find_owner
14
+
15
+ class _CustomToolTip(CTkToolTip):
16
+
17
+ def __init__(self, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+
20
+ def _show(self) -> None:
21
+ if not self.widget.winfo_exists():
22
+ self.hide()
23
+ self.destroy()
24
+
25
+ if self.status == "inside" and time.time() - self.last_moved >= self.delay:
26
+ self.status = "visible"
27
+ try:
28
+ self.deiconify()
29
+ except _tkinter.TclError:
30
+ pass
31
+
32
+
33
+ class _System():
34
+
35
+ def __init__(self) -> None:
36
+ pass
37
+
38
+ @staticmethod
39
+ def GetPath(path=None) -> str:
40
+ if path is None:
41
+ path = os.getcwd()
42
+ return f"{path}" if path == os.getenv('HOME') else path
43
+
44
+ @staticmethod
45
+ def parse_path(path):
46
+
47
+ return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
48
+
49
+ class _DrawApp():
50
+
51
+ def __init__(self,
52
+ method : str,
53
+ filetypes: Optional[List[str]] = None,
54
+ bufering: int = 1,
55
+ encoding: str = 'utf-8',
56
+ current_path : str = '.',
57
+ hidden: bool = False,
58
+ preview_img: bool = False,
59
+ autocomplete: bool = False,
60
+ video_preview: bool = False,
61
+ tool_tip: bool = False,
62
+ title: str = 'CTkFileDialog',
63
+ geometry: str = '1320x720') -> None:
64
+
65
+ self.current_path = current_path
66
+
67
+ if not self.current_path:
68
+ self.current_path = os.getcwd()
69
+ else:
70
+ self.current_path = _System.parse_path(path=self.current_path)
71
+ self.autocomplete = autocomplete
72
+
73
+ self.preview_img = preview_img
74
+ self.bufering = bufering
75
+ self.encoding = encoding
76
+ self.hidden = hidden
77
+ self.video_preview = video_preview
78
+ self.suggest = []
79
+ self.tool_tip = tool_tip
80
+ self._all_buttons = []
81
+ self.filetypes = filetypes
82
+ self.tab_index = -1
83
+ self._BASE_DIR = Path(__file__).parent
84
+ self.method = method
85
+ self.current_theme = ctk.get_appearance_mode()
86
+ self.view_mode = "grid" # Default view mode
87
+ self.display_files = [] # Files to display
88
+ self.BATCH = 50 # Load files in batches of 50
89
+ self.app = ctk.CTkToplevel()
90
+ self.app.title(string=title)
91
+ self.app.geometry(geometry)
92
+ self.selected_file = ''
93
+ self.selected_objects : list = []
94
+ self._load_icons()
95
+ self._temp_item = None
96
+ self.app.protocol("WM_DELETE_WINDOW", self.protocol_windows)
97
+ self._temp_items = []
98
+ self.TopSide(master=self.app)
99
+ self.LeftSide(master=self.app)
100
+ self.CenterSide(master=self.app)
101
+ self.app.bind("<Alt-Left>", lambda _: self.btn_back(master=self.app))
102
+ try:
103
+ self.app.grab_set()
104
+ except _tkinter.TclError:
105
+ pass
106
+
107
+ def protocol_windows(self):
108
+
109
+ try:
110
+ self.app.destroy()
111
+
112
+ self.app.unbind_all("<MouseWheel>")
113
+ except Exception:
114
+ pass
115
+
116
+ @staticmethod
117
+ def _is_image(image : str) -> bool :
118
+ try:
119
+
120
+ with Image.open(image) as img:
121
+
122
+ img.verify()
123
+
124
+ return True
125
+ except:
126
+ return False
127
+ def _load_icons(self):
128
+ icon_path = self._BASE_DIR / "icons"
129
+
130
+ self.icons = {
131
+ "folder": ctk.CTkImage(Image.open(icon_path / "folder.png"), size=(40, 40)),
132
+ "bash": ctk.CTkImage(Image.open(icon_path / "bash.png"), size=(40, 40)),
133
+ "image": ctk.CTkImage(Image.open(icon_path / "image.png"), size=(40, 40)),
134
+ "python": ctk.CTkImage(Image.open(icon_path / "python.png"), size=(40, 40)),
135
+ "text": ctk.CTkImage(Image.open(icon_path / "text.png"), size=(40, 40)),
136
+ "markdown": ctk.CTkImage(Image.open(icon_path / "markdown.png"), size=(40, 40)),
137
+ "javascript": ctk.CTkImage(Image.open(icon_path / "javascript.png"), size=(40, 40)),
138
+ "php": ctk.CTkImage(Image.open(icon_path / "php.png"), size=(40, 40)),
139
+ "html": ctk.CTkImage(Image.open(icon_path / "html.png"), size=(40, 40)),
140
+ "css": ctk.CTkImage(Image.open(icon_path / "css.png"), size=(40, 40)),
141
+ "ini": ctk.CTkImage(Image.open(icon_path / "ini.png"), size=(40, 40)),
142
+ "conf": ctk.CTkImage(Image.open(icon_path / "conf.png"), size=(40, 40)),
143
+ "exe": ctk.CTkImage(Image.open(icon_path / "exe.png"), size=(40, 40)),
144
+ "odt": ctk.CTkImage(Image.open(icon_path / "odt.png"), size=(40, 40)),
145
+ "pdf": ctk.CTkImage(Image.open(icon_path / "pdf.png"), size=(40, 40)),
146
+ "json": ctk.CTkImage(Image.open(icon_path / "json.png"), size=(40, 40)),
147
+ "gz": ctk.CTkImage(Image.open(icon_path / "gz.png"), size=(40, 40)),
148
+ "video": ctk.CTkImage(Image.open(icon_path / "video.png"), size=(40, 40)),
149
+ "awk": ctk.CTkImage(Image.open(icon_path / "bash.png"), size=(40, 40)),
150
+ 'webp': ctk.CTkImage(Image.open(icon_path / 'image.png'), size=(40, 40)),
151
+ "default": ctk.CTkImage(Image.open(icon_path / "text.png"), size=(40, 40)), # default icon
152
+ }
153
+
154
+ self.extension_icons = {
155
+ ".webp": "webp",
156
+ ".awk": "bash",
157
+ ".mp4": "video",
158
+ ".mvk": "video",
159
+ ".sh": "bash",
160
+ ".zsh": "bash",
161
+ ".py": "python",
162
+ ".png": "image",
163
+ ".jpg": "image",
164
+ ".jpeg": "image",
165
+ ".txt": "text",
166
+ ".js": "javascript",
167
+ ".md": "markdown",
168
+ ".php": "php",
169
+ ".html": "html",
170
+ ".css": "css",
171
+ ".ini": "ini",
172
+ ".conf": "conf",
173
+ ".json": "json",
174
+ ".odt": "odt",
175
+ ".pdf": "pdf",
176
+ ".exe": "exe",
177
+ ".gz": "gz",
178
+ }
179
+
180
+ def update_entry(self, path) -> None:
181
+ self.PathEntry.configure(state='normal')
182
+ self.PathEntry.delete(0, 'end')
183
+ self.PathEntry.insert(0, path)
184
+
185
+ def fix_name(self, name: str,
186
+ max_len : int = 18) -> str:
187
+
188
+ if len(name) > max_len:
189
+
190
+ return name[:max_len - 3]
191
+ return name
192
+
193
+ def btn_back(self, master: ctk.CTkToplevel):
194
+ if self.current_path != os.path.dirname(self.current_path):
195
+ self.current_path = os.path.dirname(self.current_path)
196
+ self.update_entry(path=self.current_path)
197
+ self._list_files(master)
198
+
199
+
200
+ def navigate_to(self, path: str, master):
201
+ try:
202
+ path = os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
203
+
204
+ # If it's a directory
205
+ if os.path.isdir(path):
206
+ if self.method == 'askdirectory':
207
+ self._temp_item = path
208
+ self.current_path = Path(path)
209
+ self.update_entry(path=self.current_path)
210
+ self._list_files(master)
211
+ return
212
+
213
+ # If it's a file and we're in save-as mode
214
+ if self.method in ['asksaveasfile', 'asksaveasfilename']:
215
+ if os.path.isfile(path):
216
+ msg = CTkMessagebox(
217
+ message='This file exists. Do you want to overwrite it?',
218
+ icon='warning',
219
+ title='Warning',
220
+ option_1='Yes',
221
+ option_2='No'
222
+ )
223
+ if msg.get() == 'No':
224
+ return
225
+ self._temp_item = path
226
+ self.close_app()
227
+ return
228
+
229
+ if self.method == 'askopenfile':
230
+ if not os.path.isfile(path):
231
+
232
+ CTkMessagebox(message='File not found!', title='Error', icon='cancel')
233
+ self.PathEntry.delete(0, ctk.END)
234
+ self.PathEntry.insert(0, self.current_path)
235
+ return
236
+
237
+ self._temp_item = path
238
+ self.update_entry(self._temp_item)
239
+ return
240
+
241
+ if os.path.isfile(path):
242
+ self._temp_item = path
243
+ self.update_entry(self._temp_item)
244
+ return
245
+
246
+ self.PathEntry.delete(0, 'end')
247
+ self.PathEntry.insert(0, str(self.current_path))
248
+ self.PathEntry.configure(state='normal')
249
+
250
+ CTkMessagebox(message='No such file or directory!', title='Error', icon='cancel')
251
+ return
252
+
253
+ except PermissionError:
254
+
255
+ CTkMessagebox(message='Permission denied!', title='Error', icon='cancel')
256
+ except FileNotFoundError:
257
+
258
+ CTkMessagebox(message='File Not Found!', title='Error', icon='cancel')
259
+
260
+ def close_app(self):
261
+ if self.method == 'asksaveasfilename':
262
+ if not os.path.isdir(self.PathEntry.get()): self.selected_file = self.PathEntry.get()
263
+
264
+ if self._temp_item:
265
+ self.protocol_windows()
266
+ self.app.destroy()
267
+ if self.method == 'asksaveasfile':
268
+ self.selected_file = self._temp_item
269
+ return
270
+ elif self.method == 'askopenfile':
271
+ self.selected_file = self._temp_item
272
+ else:
273
+ self.selected_file = self._temp_item
274
+ return
275
+
276
+ if len(self._temp_items) >= 1:
277
+ self.protocol_windows()
278
+ self.app.destroy()
279
+ if self.method == "askopenfilenames" or self.method == "askopenfiles":
280
+ seen = set()
281
+ self.selected_objects = [
282
+ f for f in self._temp_items
283
+ if not os.path.isdir(f) and f not in seen and not seen.add(f)
284
+ ]
285
+
286
+ return
287
+
288
+
289
+ @staticmethod
290
+ def _is_video(video: str):
291
+
292
+ try:
293
+
294
+ cap = cv2.VideoCapture(video)
295
+ valid = cap.isOpened()
296
+ cap.release()
297
+ return valid
298
+ except:
299
+
300
+ return False
301
+
302
+
303
+ def _autocomplete(self, event):
304
+
305
+ if not hasattr(self, "entire_paths"):
306
+ return "break"
307
+
308
+ if not self.entire_paths:
309
+
310
+ return "break"
311
+
312
+ if not self.files:
313
+ return "break"
314
+
315
+
316
+ max_index = len(self.files)
317
+
318
+ if event.keysym == 'Up':
319
+ self.tab_index = (self.tab_index - 1) % max_index
320
+ else:
321
+ self.tab_index = (self.tab_index + 1) % max_index
322
+
323
+ path = self.entire_paths[self.tab_index]
324
+ self.PathEntry.delete(0, ctk.END)
325
+ self.PathEntry.insert(0, path)
326
+
327
+ self._temp_item = path
328
+
329
+ return "break"
330
+
331
+ def TopSide(self, master: ctk.CTkToplevel) -> None:
332
+ TopBar = ctk.CTkFrame(master=master, height=40, fg_color="transparent")
333
+ TopBar.pack(side='top', fill='x')
334
+
335
+ def btn_exit():
336
+ msg = CTkMessagebox(message='Do you want to exit?', title='Exit', option_1='Yes', option_2='No', icon='warning')
337
+ if msg.get() == 'Yes':
338
+ self.protocol_windows()
339
+
340
+ self.selected_file = None
341
+ self.selected_objects = []
342
+ self._temp_item = None
343
+ self._temp_items = []
344
+ master.destroy()
345
+ return
346
+
347
+ # Exit button
348
+ ButtonExit = ctk.CTkButton(master=TopBar, text='Exit', font=('Hack Nerd Font', 15), width=70, command=btn_exit, hover_color='red')
349
+ ButtonExit.pack(side='left', fill='x')
350
+
351
+ # Path field
352
+ self.PathEntry = ctk.CTkEntry(master=TopBar, width=1070, corner_radius=0, insertwidth=0)
353
+ self.PathEntry.insert(index=0, string=_System.GetPath(str(self.current_path)))
354
+ self.PathEntry.pack(side='right', fill='y', padx=10, pady=10)
355
+ self.PathEntry.bind('<Return>', command = lambda _: self.navigate_to(path=self.PathEntry.get(), master=master))
356
+
357
+ # Back button
358
+ ButtonBack = ctk.CTkButton(master=TopBar, text='', font=('Hack Nerd Font', 15), width=70, command = lambda path=self.PathEntry.get(): self.btn_back(master=master))
359
+ ButtonBack.pack(side='left', fill='x', padx=10, pady=10)
360
+
361
+ # Ok button
362
+ ButtonOk = ctk.CTkButton(master=TopBar, text='Ok', font=('Hack Nerd Font', 15), width=70, command = lambda: self.close_app())
363
+ ButtonOk.pack(side='left', fill='x', padx=10, pady=10)
364
+
365
+ if self.autocomplete:
366
+
367
+ self.PathEntry.bind('<Down>', lambda event: self._autocomplete(event))
368
+ self.PathEntry.bind('<Up>', lambda event: self._autocomplete(event))
369
+ self.PathEntry.bind('<Tab>', lambda event: self._autocomplete(event))
370
+
371
+ # Search bar
372
+ self.SearchFrame = ctk.CTkFrame(master=master, fg_color="transparent", height=40)
373
+ self.SearchFrame.pack(side='top', fill='x', padx=10, pady=(5, 10))
374
+
375
+ search_label = ctk.CTkLabel(self.SearchFrame, text="Search:", font=("Arial", 12))
376
+ search_label.pack(side="left", padx=(0, 10))
377
+
378
+ self.SearchEntry = ctk.CTkEntry(self.SearchFrame, placeholder_text="Type to search files...")
379
+ self.SearchEntry.pack(expand=True, fill="x", side="left", padx=(0, 20))
380
+ self.SearchEntry.bind('<KeyRelease>', lambda _: self._search_files_default())
381
+
382
+ # View mode toggle (Grid/List)
383
+ self.view_mode = "grid"
384
+ view_label = ctk.CTkLabel(self.SearchFrame, text="View:", font=("Arial", 12))
385
+ view_label.pack(side="left", padx=(0, 10))
386
+
387
+ self.grid_btn = ctk.CTkButton(self.SearchFrame, text="📊 Grid", width=60,
388
+ command=lambda: self._set_view_mode("grid"))
389
+ self.grid_btn.pack(side="left", padx=5)
390
+
391
+ self.list_btn = ctk.CTkButton(self.SearchFrame, text="📋 List", width=60,
392
+ command=lambda: self._set_view_mode("list"))
393
+ self.list_btn.pack(side="left", padx=5)
394
+
395
+ # Sort dropdown
396
+ sort_label = ctk.CTkLabel(self.SearchFrame, text="Sort:", font=("Arial", 12))
397
+ sort_label.pack(side="left", padx=(20, 10))
398
+
399
+ self.sort_var = ctk.StringVar(value="name")
400
+ self.sort_menu = ctk.CTkOptionMenu(
401
+ self.SearchFrame,
402
+ values=["name", "date", "type", "size", "modified"],
403
+ command=self._on_sort_change,
404
+ variable=self.sort_var
405
+ )
406
+ self.sort_menu.pack(side="left", padx=5)
407
+
408
+ def _get_video_frame(self, path: str, frame_number: int = 1) -> Image.Image | None:
409
+ if not self._is_video(path):
410
+ return None
411
+
412
+ try:
413
+ cap = cv2.VideoCapture(path)
414
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
415
+ ret, frame = cap.read()
416
+ cap.release()
417
+ if ret:
418
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
419
+ return Image.fromarray(frame)
420
+ except:
421
+ return None
422
+
423
+
424
+ def LeftSide(self, master) -> None:
425
+
426
+ # Main frame
427
+ LeftSideFrame = ctk.CTkFrame(master=master, width=200)
428
+ LeftSideFrame.pack(side='left', fill='y', padx=10, pady=10)
429
+ LeftSideFrame.pack_propagate(False)
430
+
431
+ # Start with the user's HOME directory
432
+ home = os.path.expanduser("~")
433
+ folders = {f"{str(os.getenv('HOME')).replace('/home/', '')}": home}
434
+
435
+ # Load the user-dirs.dirs file
436
+ dir_file = os.path.join(home, ".config/user-dirs.dirs")
437
+ pattern = re.compile(r'XDG_\w+_DIR="(.+?)"')
438
+
439
+ import platform
440
+ if platform.system() == 'Linux':
441
+ if not os.path.exists(path=dir_file):
442
+ raise FileNotFoundError(f"The file {dir_file} is required for the program to run!")
443
+ with open(dir_file, 'r') as f:
444
+ for line in f:
445
+ if not line.startswith('#') and line.strip():
446
+ match = pattern.search(line)
447
+ if match:
448
+ path = os.path.expandvars(match.group(1))
449
+ name = os.path.basename(os.path.normpath(path))
450
+ if name != f"{os.getenv('USER')}": # Avoid duplicate
451
+ folders[name] = path
452
+
453
+ elif platform.system() == 'Windows':
454
+ home = Path.home()
455
+ win_folders = {
456
+ home.name: str(home),
457
+ "Desktop": home / "Desktop",
458
+ "Documents": home / "Documents",
459
+ "Downloads": home / "Downloads",
460
+ "Pictures": home / "Pictures",
461
+ "Music": home / "Music",
462
+ "Videos": home / "Videos",
463
+ }
464
+
465
+ folders = {}
466
+ folders = {k: v for k, v in win_folders.items()}
467
+
468
+ # Title
469
+ LabelSide = ctk.CTkLabel(master=LeftSideFrame, text='Places', font=('Hack Nerd Font', 15))
470
+ LabelSide.pack(side=ctk.TOP, padx=5, pady=5)
471
+
472
+ icons = {
473
+ os.getenv("USER"): "", # user's HOME
474
+ "Desktop": "",
475
+ "Downloads": "",
476
+ "Documents": "",
477
+ "Pictures": "",
478
+ "Music": "",
479
+ "Videos": "",
480
+ "Templates": "",
481
+ "Public": "",
482
+ }
483
+
484
+ for name, path in folders.items():
485
+ icon = icons.get(name, "")
486
+ button_text = f" {icon} {name}"
487
+ DirectorySide = ctk.CTkButton(
488
+ master=LeftSideFrame,
489
+ text=button_text,
490
+ font=("Hack Nerd Font", 14),
491
+ anchor="w",
492
+ fg_color="transparent",
493
+ hover_color="#8da3ae",
494
+ text_color="#000000" if self.current_theme.lower() == 'light' else '#cccccc',
495
+ corner_radius=2,
496
+ border_width=0,
497
+ command=lambda r=path, n=name: self.navigate_to(path=r, master=master)
498
+ )
499
+ DirectorySide.pack(fill="x", pady=4)
500
+
501
+
502
+ def event_scroll(self):
503
+
504
+ canvas = self.CenterSideFrame._parent_canvas
505
+
506
+ def _on_mousewheel(event):
507
+ try:
508
+ x_root, y_root = event.x_root, event.y_root
509
+
510
+ # Coordinates and size of the scrollable frame
511
+ x1 = self.CenterSideFrame.winfo_rootx()
512
+ y1 = self.CenterSideFrame.winfo_rooty()
513
+ x2 = x1 + self.CenterSideFrame.winfo_width()
514
+ y2 = y1 + self.CenterSideFrame.winfo_height()
515
+ if x1 <= x_root <= x2 and y1 <= y_root <= y2:
516
+
517
+ if event.num == 4:
518
+ canvas.yview_scroll(-1, "units")
519
+ elif event.num == 5:
520
+ canvas.yview_scroll(1, "units")
521
+ else:
522
+ canvas.yview_scroll(-int(event.delta / 120), "units")
523
+
524
+ # Trigger lazy loading check
525
+ self._check_scroll(self.app)
526
+ return "break"
527
+ except Exception as e:
528
+ pass
529
+
530
+ canvas.bind_all("<MouseWheel>", _on_mousewheel)
531
+ canvas.bind("<Button-4>", _on_mousewheel)
532
+ canvas.bind("<Button-5>", _on_mousewheel)
533
+ canvas.bind("<MouseWheel>", _on_mousewheel)
534
+
535
+ # Bind to all child widgets
536
+ for widget in canvas.winfo_children():
537
+ widget.bind("<MouseWheel>", _on_mousewheel)
538
+ widget.bind("<Button-4>", _on_mousewheel)
539
+ widget.bind("<Button-5>", _on_mousewheel)
540
+
541
+
542
+ def CenterSide(self, master: ctk.CTkToplevel) -> None:
543
+ self.CenterSideFrame = ctk.CTkScrollableFrame(master=master)
544
+ self.CenterSideFrame.pack(expand=True, side='top', fill='both', padx=10, pady=10)
545
+
546
+
547
+ self.event_scroll()
548
+
549
+ self.content_frame = ctk.CTkFrame(master=self.CenterSideFrame)
550
+ self.content_frame.pack(side='top', fill='both', expand=True, padx=20, pady=10)
551
+
552
+ self._list_files(master=master)
553
+
554
+ def __clear__(self):
555
+
556
+ for widget in self.content_frame.winfo_children():
557
+ try:
558
+ widget.destroy()
559
+ except (_tkinter.TclError, Exception):
560
+ pass
561
+
562
+ def _handle_click(self, event, r, master, boton, tool_tip=None):
563
+ if not event.state & 0x0004:
564
+ self._temp_items.clear()
565
+ self.selected_objects.clear()
566
+
567
+ if event.state & 0x0004:
568
+
569
+ if self.method in ['askopenfilenames', 'askopenfiles']:
570
+ if r not in self._temp_items:
571
+ self._temp_items.append(r)
572
+ boton.configure(fg_color="blue")
573
+ return
574
+
575
+ if boton not in self._all_buttons:
576
+ self._all_buttons.append(boton)
577
+
578
+
579
+ else:
580
+ self._temp_items.clear()
581
+ if self.method in ['askopenfilenames', 'askopenfiles']:
582
+ self._temp_items.append(r)
583
+
584
+ for btn in self._all_buttons:
585
+ if btn.winfo_exists():
586
+ btn.configure(fg_color="transparent",
587
+ hover_color="#8da3ae",
588
+ text_color="#000000" if self.current_theme.lower() == 'light' else '#cccccc',
589
+ )
590
+ if os.path.isdir(r):
591
+ self.navigate_to(path=r, master=master)
592
+ else:
593
+ self._temp_items.append(r)
594
+
595
+ @staticmethod
596
+ def _get_info(path: str) -> str:
597
+ try:
598
+ st = os.stat(path)
599
+
600
+ # owner user
601
+ owner = find_owner(path)
602
+
603
+ # Permissions (e.g., -rw-r--r--)
604
+ #permissions = get_permissions(path)
605
+
606
+ # readable date
607
+ fecha = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(st.st_ctime))
608
+
609
+ return f"""File: {os.path.basename(path)}
610
+ creation: {fecha}
611
+ owner: {owner}
612
+ path: {path}
613
+ """
614
+ except Exception as e:
615
+ return f"Error getting info: {e}"
616
+
617
+
618
+ def _load_files(self, master: Any, cantidad: int):
619
+ columnas = 5
620
+ row = self.LOADED // columnas
621
+ col = self.LOADED % columnas
622
+ path = self.current_path
623
+
624
+ while self.LOADED < len(self.files) and cantidad > 0:
625
+
626
+ file = self.files[self.LOADED]
627
+ full_path = os.path.join(path, file)
628
+
629
+ if self.method == 'askdirectory' and os.path.isfile(full_path):
630
+ self.LOADED += 1
631
+ continue
632
+
633
+ # Get icon based on file type
634
+ if os.path.isdir(full_path):
635
+ icon = self.icons["folder"]
636
+ else:
637
+ if self.preview_img and self._is_image(full_path):
638
+ try:
639
+ img = Image.open(full_path)
640
+ img.thumbnail((32, 32))
641
+ icon = ctk.CTkImage(light_image=img, dark_image=img, size=(32, 32))
642
+ except:
643
+ icon = self.icons.get("image", self.icons["default"])
644
+ elif self.video_preview and self._is_video(full_path):
645
+ frame = self._get_video_frame(full_path, frame_number=10)
646
+ if frame:
647
+ frame.thumbnail((32, 32))
648
+ icon = ctk.CTkImage(light_image=frame, dark_image=frame, size=(32, 32))
649
+ else:
650
+ icon = self.icons.get("video", self.icons["default"])
651
+ else:
652
+ ext = os.path.splitext(file)[1].lower()
653
+ icon_key = self.extension_icons.get(ext, "default")
654
+ icon = self.icons.get(icon_key, self.icons["default"])
655
+
656
+ fixed_name = self.fix_name(name=file)
657
+
658
+ command = None
659
+ if self.method not in ['askopenfilenames']:
660
+ command = lambda r=full_path: self.navigate_to(path=r, master=master)
661
+
662
+ boton = ctk.CTkButton(
663
+ master=self.content_frame,
664
+ text=fixed_name,
665
+ image=icon,
666
+ compound="left",
667
+ width=180,
668
+ height=60,
669
+ anchor="w",
670
+ fg_color="transparent",
671
+ hover_color="#8da3ae",
672
+ text_color="#000000" if self.current_theme.lower() == 'light' else '#cccccc',
673
+ command=command
674
+ )
675
+
676
+ if self.tool_tip:
677
+ _CustomToolTip(widget=boton, message=self._get_info(full_path))
678
+ if self.method in ['askopenfilenames', 'askopenfiles']:
679
+ boton.bind('<Button-1>', lambda event, r=full_path, b=boton: self._handle_click(event, r, master, b))
680
+ boton.grid(row=row, column=col, padx=10, pady=10)
681
+ col += 1
682
+ if col >= columnas:
683
+ col = 0
684
+ row += 1
685
+
686
+ self.LOADED += 1
687
+ cantidad -= 1
688
+
689
+ def _check_scroll(self, master):
690
+ try:
691
+ canvas = self.CenterSideFrame._parent_canvas
692
+ yview = canvas.yview()
693
+
694
+ # Ensure display_files exists
695
+ if not hasattr(self, 'display_files') or not self.display_files:
696
+ return
697
+
698
+ # When user scrolls near the bottom, load more files
699
+ if yview[1] > 0.80 and self.LOADED < len(self.display_files):
700
+ if self.view_mode == "grid":
701
+ self._load_grid_files(self.BATCH)
702
+ else:
703
+ self._load_list_files(self.BATCH)
704
+ except _tkinter.TclError:
705
+ pass
706
+
707
+ def _search_files_default(self):
708
+ # Only search if files have been loaded
709
+ if not hasattr(self, 'files') or not self.files:
710
+ return
711
+
712
+ search_query = self.SearchEntry.get().lower()
713
+
714
+ # Clear current display
715
+ self.__clear__()
716
+
717
+ if not search_query:
718
+ # If search is empty, reload all files
719
+ self._list_files(self.app)
720
+ return
721
+
722
+ # Filter files based on search query
723
+ filtered_files = [f for f in self.files if search_query in f.lower()]
724
+
725
+ # Display sorted results with lazy loading
726
+ sorted_filtered = self._sort_files(filtered_files)
727
+ self._display_files(sorted_filtered)
728
+
729
+ def _set_view_mode(self, mode: str):
730
+ """Toggle between grid and list view"""
731
+ self.view_mode = mode
732
+
733
+ # Update button styles
734
+ if mode == "grid":
735
+ self.grid_btn.configure(fg_color="blue")
736
+ self.list_btn.configure(fg_color="gray30")
737
+ else:
738
+ self.grid_btn.configure(fg_color="gray30")
739
+ self.list_btn.configure(fg_color="blue")
740
+
741
+ # Refresh display
742
+ self._list_files(self.app)
743
+
744
+ def _on_sort_change(self, value):
745
+ """Handle sort option change"""
746
+ self._list_files(self.app)
747
+
748
+ def _sort_files(self, files: list) -> list:
749
+ """Sort files based on selected criteria"""
750
+ sort_by = self.sort_var.get()
751
+ current_path = self.current_path
752
+
753
+ def get_file_info(filename):
754
+ full_path = os.path.join(current_path, filename)
755
+ try:
756
+ stat_info = os.stat(full_path)
757
+ return {
758
+ 'name': filename.lower(),
759
+ 'date': stat_info.st_mtime,
760
+ 'modified': stat_info.st_mtime,
761
+ 'type': os.path.splitext(filename)[1].lower(),
762
+ 'size': stat_info.st_size,
763
+ 'is_dir': os.path.isdir(full_path)
764
+ }
765
+ except:
766
+ return {
767
+ 'name': filename.lower(),
768
+ 'date': 0,
769
+ 'modified': 0,
770
+ 'type': '',
771
+ 'size': 0,
772
+ 'is_dir': os.path.isdir(full_path)
773
+ }
774
+
775
+ # Sort: directories first, then by selected criteria
776
+ sorted_files = sorted(
777
+ files,
778
+ key=lambda f: (not get_file_info(f)['is_dir'], get_file_info(f).get(sort_by, 0))
779
+ )
780
+
781
+ return sorted_files
782
+
783
+ def _display_files(self, files: list):
784
+ """Display files in current view mode (rollback: load all at once)."""
785
+ # Store files and load all at once to restore previous behavior
786
+ self.display_files = files
787
+ self.LOADED = 0
788
+
789
+ total = len(files)
790
+ if total <= 0:
791
+ return
792
+
793
+ # Load all items immediately
794
+ if self.view_mode == "grid":
795
+ self._load_grid_files(total)
796
+ else:
797
+ self._load_list_files(total)
798
+
799
+ def _load_grid_files(self, cantidad: int):
800
+ """Incrementally load grid view files"""
801
+ columnas = 5
802
+
803
+ while self.LOADED < len(self.display_files) and cantidad > 0:
804
+ file = self.display_files[self.LOADED]
805
+
806
+ if self.method == 'askdirectory' and os.path.isfile(os.path.join(self.current_path, file)):
807
+ self.LOADED += 1
808
+ continue
809
+
810
+ full_path = os.path.join(self.current_path, file)
811
+
812
+ # Get icon
813
+ if os.path.isdir(full_path):
814
+ icon = self.icons["folder"]
815
+ else:
816
+ if self.preview_img and self._is_image(full_path):
817
+ try:
818
+ img = Image.open(full_path)
819
+ img.thumbnail((32, 32))
820
+ icon = ctk.CTkImage(light_image=img, dark_image=img, size=(32, 32))
821
+ except:
822
+ icon = self.icons.get("image", self.icons["default"])
823
+ elif self.video_preview and self._is_video(full_path):
824
+ frame = self._get_video_frame(full_path, frame_number=10)
825
+ if frame:
826
+ frame.thumbnail((32, 32))
827
+ icon = ctk.CTkImage(light_image=frame, dark_image=frame, size=(32, 32))
828
+ else:
829
+ icon = self.icons.get("video", self.icons["default"])
830
+ else:
831
+ ext = os.path.splitext(file)[1].lower()
832
+ icon_key = self.extension_icons.get(ext, "default")
833
+ icon = self.icons.get(icon_key, self.icons["default"])
834
+
835
+ fixed_name = self.fix_name(name=file)
836
+
837
+ command = None
838
+ if self.method not in ['askopenfilenames']:
839
+ command = lambda r=full_path: self.navigate_to(path=r, master=self.app)
840
+
841
+ row = self.LOADED // columnas
842
+ col = self.LOADED % columnas
843
+
844
+ boton = ctk.CTkButton(
845
+ master=self.content_frame,
846
+ text=fixed_name,
847
+ image=icon,
848
+ compound="left",
849
+ width=180,
850
+ height=60,
851
+ anchor="w",
852
+ fg_color="transparent",
853
+ hover_color="#8da3ae",
854
+ text_color="#000000" if self.current_theme.lower() == 'light' else '#cccccc',
855
+ command=command
856
+ )
857
+
858
+ if self.tool_tip:
859
+ _CustomToolTip(widget=boton, message=self._get_info(full_path))
860
+ if self.method in ['askopenfilenames', 'askopenfiles']:
861
+ boton.bind('<Button-1>', lambda event, r=full_path, b=boton: self._handle_click(event, r, self.app, b))
862
+ boton.grid(row=row, column=col, padx=10, pady=10)
863
+
864
+ self.LOADED += 1
865
+ cantidad -= 1
866
+
867
+ # Force update scroll region
868
+ try:
869
+ self.content_frame.update_idletasks()
870
+ self.CenterSideFrame._parent_canvas.configure(scrollregion=self.CenterSideFrame._parent_canvas.bbox("all"))
871
+ except:
872
+ pass
873
+
874
+ def _show_load_more_button(self):
875
+ """Show a manual 'Load more' button at the end of the content frame."""
876
+ try:
877
+ if hasattr(self, '_load_more_btn') and getattr(self, '_load_more_btn') and self._load_more_btn.winfo_exists():
878
+ return
879
+
880
+ self._load_more_btn = ctk.CTkButton(master=self.content_frame, text="Load more",
881
+ command=self._on_load_more)
882
+ # place it at the bottom of the content frame
883
+ self._load_more_btn.pack(side='top', pady=10)
884
+ except Exception:
885
+ pass
886
+
887
+ def _remove_load_more_button(self):
888
+ """Remove the manual 'Load more' button if present."""
889
+ try:
890
+ if hasattr(self, '_load_more_btn') and getattr(self, '_load_more_btn') and self._load_more_btn.winfo_exists():
891
+ try:
892
+ self._load_more_btn.destroy()
893
+ except Exception:
894
+ pass
895
+ if hasattr(self, '_load_more_btn'):
896
+ try:
897
+ del self._load_more_btn
898
+ except Exception:
899
+ pass
900
+ except Exception:
901
+ pass
902
+
903
+ def _on_load_more(self):
904
+ """Handler for the manual load-more button."""
905
+ try:
906
+ remaining = len(self.display_files) - self.LOADED
907
+ if remaining <= 0:
908
+ self._remove_load_more_button()
909
+ return
910
+
911
+ cantidad = self.BATCH if remaining >= self.BATCH else remaining
912
+ if self.view_mode == 'grid':
913
+ self._load_grid_files(cantidad)
914
+ else:
915
+ self._load_list_files(cantidad)
916
+
917
+ # If we've finished loading, remove the button
918
+ if self.LOADED >= len(self.display_files):
919
+ self._remove_load_more_button()
920
+ except Exception:
921
+ pass
922
+
923
+ def _load_list_files(self, cantidad: int):
924
+ """Incrementally load list view files"""
925
+ while self.LOADED < len(self.display_files) and cantidad > 0:
926
+ file = self.display_files[self.LOADED]
927
+
928
+ if self.method == 'askdirectory' and os.path.isfile(os.path.join(self.current_path, file)):
929
+ self.LOADED += 1
930
+ continue
931
+
932
+ full_path = os.path.join(self.current_path, file)
933
+
934
+ # Get file info
935
+ try:
936
+ stat_info = os.stat(full_path)
937
+ file_size = stat_info.st_size
938
+ mod_time = time.strftime('%Y-%m-%d %H:%M', time.localtime(stat_info.st_mtime))
939
+ except:
940
+ file_size = 0
941
+ mod_time = "N/A"
942
+
943
+ is_dir = os.path.isdir(full_path)
944
+ file_type = "Directory" if is_dir else os.path.splitext(file)[1][1:].upper() or "File"
945
+
946
+ # Get icon
947
+ if is_dir:
948
+ icon = self.icons["folder"]
949
+ else:
950
+ ext = os.path.splitext(file)[1].lower()
951
+ icon_key = self.extension_icons.get(ext, "default")
952
+ icon = self.icons.get(icon_key, self.icons["default"])
953
+
954
+ # Size formatting
955
+ if file_size < 1024:
956
+ size_str = f"{file_size} B"
957
+ elif file_size < 1024 * 1024:
958
+ size_str = f"{file_size / 1024:.1f} KB"
959
+ else:
960
+ size_str = f"{file_size / (1024 * 1024):.1f} MB"
961
+
962
+ # Create list item frame
963
+ item_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent", height=40)
964
+ item_frame.pack(fill="x", padx=10, pady=5)
965
+
966
+ # Icon
967
+ icon_label = ctk.CTkLabel(item_frame, image=icon, text="")
968
+ icon_label.pack(side="left", padx=10)
969
+
970
+ # File name and details
971
+ info_text = f"{file}\n{file_type} • {size_str} • {mod_time}"
972
+
973
+ command = None
974
+ if self.method not in ['askopenfilenames']:
975
+ command = lambda r=full_path: self.navigate_to(path=r, master=self.app)
976
+
977
+ boton = ctk.CTkButton(
978
+ master=item_frame,
979
+ text=info_text,
980
+ compound="left",
981
+ anchor="w",
982
+ fg_color="transparent",
983
+ hover_color="#8da3ae",
984
+ text_color="#000000" if self.current_theme.lower() == 'light' else '#cccccc',
985
+ command=command,
986
+ font=("Arial", 11)
987
+ )
988
+ boton.pack(expand=True, fill="both", side="left")
989
+
990
+ if self.method in ['askopenfilenames', 'askopenfiles']:
991
+ boton.bind('<Button-1>', lambda event, r=full_path, b=boton: self._handle_click(event, r, self.app, b))
992
+
993
+ self.LOADED += 1
994
+ cantidad -= 1
995
+
996
+ # Force update scroll region
997
+ try:
998
+ self.content_frame.update_idletasks()
999
+ self.CenterSideFrame._parent_canvas.configure(scrollregion=self.CenterSideFrame._parent_canvas.bbox("all"))
1000
+ except:
1001
+ pass
1002
+
1003
+
1004
+ def _list_files(self, master: ctk.CTkToplevel) -> None:
1005
+ self.LOADED = 0
1006
+ self.BATCH = 50
1007
+ self.selected_objects.clear()
1008
+ self._all_buttons.clear()
1009
+
1010
+ self.CenterSideFrame._parent_canvas.yview_moveto(0)
1011
+ self.__clear__()
1012
+
1013
+ path = self.current_path
1014
+
1015
+ self.files = [
1016
+ f.name for f in os.scandir(path)
1017
+ if (
1018
+ (f.is_dir() or (self.method != 'askdirectory' and f.is_file())) and
1019
+ (self.hidden or not f.name.startswith('.')) and
1020
+ (f.is_dir() or not self.filetypes or
1021
+ any(f.name.endswith(ext) for ext in self.filetypes))
1022
+ )
1023
+ ]
1024
+
1025
+ if not self.files:
1026
+ self.display_files = []
1027
+ return
1028
+
1029
+ if self.autocomplete:
1030
+ self.entire_paths = [os.path.join(self.current_path, f) for f in self.files]
1031
+
1032
+ if not self.entire_paths:
1033
+ self.entire_paths = None
1034
+
1035
+ # Sort files before displaying
1036
+ sorted_files = self._sort_files(self.files)
1037
+ self._display_files(sorted_files)
1038
+
1039
+
1040
+ class _MiniDialog():
1041
+
1042
+ def __init__(self,
1043
+ method: str,
1044
+ hidden: bool = False,
1045
+ filetypes: Optional[List[str]] = None,
1046
+ autocomplete: bool = False,
1047
+ initial_dir: str = '.',
1048
+ _extra_method: str = '',
1049
+ geometry: str = '500x400',
1050
+ title: str = 'CTkFileDialog'):
1051
+
1052
+ self.master = ctk.CTkToplevel()
1053
+ self.master.geometry(geometry_string=geometry)
1054
+ self.master.title(title)
1055
+ self._extra_method = _extra_method
1056
+ self.tab_index = -1
1057
+ self.method = method
1058
+ self.hidden = hidden
1059
+ self.filetypes = filetypes
1060
+ self.autocomplete = autocomplete
1061
+ self.initial_dir = initial_dir
1062
+
1063
+ if not self.initial_dir:
1064
+ self.initial_dir = os.getcwd()
1065
+ else:
1066
+ self.initial_dir = _System().GetPath(path=self.initial_dir)
1067
+
1068
+ self.selected_path = ''
1069
+ self.selected_paths = []
1070
+ self.selected_items = []
1071
+ self.selected_item = ''
1072
+
1073
+ # Load images
1074
+ self._PATH = os.path.dirname(os.path.realpath(__file__))
1075
+
1076
+ self.folder_image = self._load_image(image=os.path.join(self._PATH, 'icons/_IconsMini/folder.png'))
1077
+
1078
+ self.file_image = self._load_image(image=os.path.join(self._PATH, "icons/_IconsMini/file.png"))
1079
+
1080
+ self._TopSide()
1081
+
1082
+ self._CenterSide()
1083
+
1084
+ self.list_files()
1085
+ self.master.bind("<Alt-Left>", lambda _: self._up() )
1086
+
1087
+ self.master.wait_visibility()
1088
+ self.master.grab_set()
1089
+ self.master.wait_window()
1090
+
1091
+
1092
+ def _get_path(self):
1093
+
1094
+ return os.path.abspath(os.path.expandvars(os.path.expanduser(self.initial_dir)))
1095
+
1096
+ def _TopSide(self):
1097
+
1098
+ self.frame = ctk.CTkFrame(self.master)
1099
+ self.frame.pack(fill=ctk.BOTH, expand=True)
1100
+
1101
+ self.path_frame = ctk.CTkFrame(self.frame)
1102
+ self.path_frame.pack(fill=ctk.X, padx=10, pady=10)
1103
+
1104
+ self.path_entry = ctk.CTkEntry(self.path_frame, )
1105
+ self.path_entry.pack(expand=True, fill=ctk.X, side=ctk.LEFT, padx=10, pady=10)
1106
+ self.path_entry.bind('<Return>', lambda _: self._on_enter_path())
1107
+ self.path_entry.insert(0, self._get_path())
1108
+
1109
+ if self.autocomplete:
1110
+ for bind in ['<Tab>', '<Down>', '<Up>']:
1111
+ self.path_entry.bind(bind, self._autocomplete)
1112
+
1113
+ self.up_btn = ctk.CTkButton(
1114
+ self.path_frame, text="↑", width=30, command=self._up
1115
+ )
1116
+
1117
+ self.up_btn.pack(side=ctk.RIGHT, padx=10, pady=10)
1118
+
1119
+ # Search bar
1120
+ self.search_frame = ctk.CTkFrame(self.frame)
1121
+ self.search_frame.pack(fill=ctk.X, padx=10, pady=(0, 10))
1122
+
1123
+ search_label = ctk.CTkLabel(self.search_frame, text="Search:", font=("Arial", 12))
1124
+ search_label.pack(side=ctk.LEFT, padx=(0, 10))
1125
+
1126
+ self.search_entry = ctk.CTkEntry(self.search_frame, placeholder_text="Type to search files...")
1127
+ self.search_entry.pack(expand=True, fill=ctk.X, side=ctk.LEFT)
1128
+ self.search_entry.bind('<KeyRelease>', lambda _: self._search_files())
1129
+ btn_frame = ctk.CTkFrame(self.frame, fg_color='transparent')
1130
+ btn_frame.pack(side=ctk.BOTTOM, fill=ctk.X, padx=10, pady=10)
1131
+
1132
+ ok_btn = ctk.CTkButton(btn_frame, text="OK", command=self._on_select)
1133
+ ok_btn.pack(side=ctk.RIGHT)
1134
+
1135
+ ctk.CTkButton(btn_frame, text="Cancel", command=self._on_cancel).pack(
1136
+ side=ctk.RIGHT, padx=10
1137
+ )
1138
+
1139
+ def list_files(self):
1140
+ path = os.path.abspath(os.path.expanduser(os.path.expandvars(self.path_entry.get())))
1141
+ if os.path.isfile(path):
1142
+ return
1143
+ try:
1144
+ try:
1145
+ for item in self.tree.get_children():
1146
+ self.tree.delete(item)
1147
+ except TclError:
1148
+ return
1149
+
1150
+ self.files = {'name': [], 'path': []}
1151
+ filtered = []
1152
+
1153
+ for f in os.scandir(path):
1154
+ if (
1155
+ (f.is_dir() or (self.method != 'askdirectory' and f.is_file())) and
1156
+ (self.hidden or not f.name.startswith('.')) and
1157
+ (f.is_dir() or not self.filetypes or
1158
+ any(f.name.endswith(ext) for ext in self.filetypes))
1159
+ ):
1160
+ filtered.append(f)
1161
+ self.files['name'].append(f.name)
1162
+ self.files['path'].append(f.path)
1163
+
1164
+ sorted_files = sorted(
1165
+ filtered,
1166
+ key=lambda f: (not f.is_dir(), f.name.lower())
1167
+ )
1168
+
1169
+ self.update_entry(path=path)
1170
+
1171
+ for f in sorted_files:
1172
+ icon = self.folder_image if f.is_dir() else self.file_image
1173
+ self.tree.insert("", tk.END, text=f.name, image=icon)
1174
+
1175
+ if self.autocomplete:
1176
+ self.absolute_paths = [f.path for f in sorted_files]
1177
+
1178
+ except PermissionError:
1179
+ CTkMessagebox(message='Permission Denied!', title='Error', icon='cancel')
1180
+ self._on_cancel(destroy=False)
1181
+ else:
1182
+ if self.autocomplete:
1183
+ self.max_index = len(self.files['name'])
1184
+
1185
+
1186
+ def update_entry(self, path):
1187
+ self.path_entry.configure(state='normal')
1188
+ self.path_entry.delete(0, ctk.END)
1189
+ self.path_entry.insert(0, path)
1190
+
1191
+ def _autocomplete(self, event: tk.Event):
1192
+
1193
+ if not self.files['name'] or not hasattr(self, "max_index"):
1194
+ return "break"
1195
+
1196
+ if event.keysym == 'Up':
1197
+ self.tab_index = (self.tab_index - 1) % self.max_index
1198
+ else:
1199
+ self.tab_index = (self.tab_index + 1) % self.max_index
1200
+
1201
+ path = self.absolute_paths[self.tab_index]
1202
+
1203
+ self.path_entry.delete(0, ctk.END)
1204
+ self.path_entry.insert(0, path)
1205
+
1206
+ item_id = self.tree.get_children()[self.tab_index]
1207
+ self.tree.focus(item_id)
1208
+ self.tree.selection_set(item_id)
1209
+ self.tree.see(item_id)
1210
+
1211
+ self.selected_item = path
1212
+ return "break"
1213
+
1214
+ def _on_enter_path(self):
1215
+ path = os.path.abspath(os.path.expanduser(os.path.expandvars(self.path_entry.get())))
1216
+
1217
+ if os.path.isdir(path):
1218
+ self.initial_dir = path
1219
+ self.list_files()
1220
+ else:
1221
+ if os.path.isfile(path):
1222
+ return
1223
+
1224
+ self.path_entry.configure(state='normal')
1225
+
1226
+ if not os.path.exists(path=path):
1227
+
1228
+ self._on_cancel(destroy=False)
1229
+ self.update_entry(path=self.initial_dir)
1230
+ CTkMessagebox(title="Error", icon='cancel', message='No such file or directory!')
1231
+
1232
+ return ""
1233
+
1234
+
1235
+ def _on_cancel(self, destroy: bool = True):
1236
+ self.selected_path = None
1237
+ self.selected_item = None
1238
+
1239
+ self.selected_paths = None
1240
+ self.selected_items = None
1241
+ if destroy:
1242
+ self.master.destroy()
1243
+ return
1244
+
1245
+ def _CenterSide(self):
1246
+ self.tree_frame = ctk.CTkFrame(self.frame)
1247
+ self.tree_frame.pack(fill=ctk.BOTH, expand=True, padx=10, pady=5)
1248
+
1249
+ style = ttk.Style()
1250
+ style.theme_use('clam')
1251
+ mode = ctk.get_appearance_mode()
1252
+
1253
+ if mode == 'Dark':
1254
+ style.configure("Treeview",
1255
+ background="#242424",
1256
+ foreground="#FFFFFF",
1257
+ fieldbackground="#242424",
1258
+ bordercolor="#242424",
1259
+ rowheight=30)
1260
+ style.map("Treeview",
1261
+ background=[('selected', '#444444')],
1262
+ foreground=[('selected', '#FFFFFF')])
1263
+ else: # Light mode
1264
+ style.configure("Treeview",
1265
+ background="#FFFFFF",
1266
+ foreground="#000000",
1267
+ fieldbackground="#FFFFFF",
1268
+ bordercolor="#DDDDDD",
1269
+ rowheight=30)
1270
+ style.map("Treeview",
1271
+ background=[('selected', '#E0E0E0')],
1272
+ foreground=[('selected', '#000000')])
1273
+ self.tree = ttk.Treeview(self.tree_frame, show="tree", selectmode='extended' if self.method in ['askopenfilenames', 'askopenfiles'] else 'browse')
1274
+ self.tree.bind("<Double-1>", self._on_click)
1275
+ self.tree.bind("<Button-1>", self._on_select_item)
1276
+ self.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
1277
+
1278
+ def _load_image(self, image: str) -> tk.PhotoImage:
1279
+
1280
+ return tk.PhotoImage(file=image)
1281
+
1282
+ def _search_files(self):
1283
+ # Only search if files have been loaded
1284
+ if not hasattr(self, 'files') or not self.files['name']:
1285
+ return
1286
+
1287
+ search_query = self.search_entry.get().lower()
1288
+
1289
+ # Clear current tree
1290
+ for item in self.tree.get_children():
1291
+ self.tree.delete(item)
1292
+
1293
+ if not search_query:
1294
+ # If search is empty, reload all files
1295
+ self.list_files()
1296
+ return
1297
+
1298
+ # Filter files based on search query
1299
+ self.filtered_paths = []
1300
+ for name, path in zip(self.files['name'], self.files['path']):
1301
+ if search_query in name.lower():
1302
+ is_dir = os.path.isdir(path)
1303
+ icon = self.folder_image if is_dir else self.file_image
1304
+ self.tree.insert("", tk.END, text=name, image=icon)
1305
+ self.filtered_paths.append(path)
1306
+
1307
+ # Store the filtered paths so _on_select_item and _on_click can use them
1308
+ self.absolute_paths = self.filtered_paths
1309
+
1310
+ def _on_select(self):
1311
+
1312
+ path = self.path_entry.get().strip() if hasattr(self, "path_entry") else ""
1313
+
1314
+ if path:
1315
+ path = os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
1316
+ if not os.path.dirname(path):
1317
+ path = os.path.join(self.initial_dir, path)
1318
+
1319
+ if self.method in ['asksaveasfile', 'asksaveasfilename']:
1320
+ if not path or os.path.isdir(path):
1321
+ return
1322
+
1323
+ if os.path.exists(path) and self._extra_method != 'askopenfile':
1324
+ opts = CTkMessagebox(
1325
+ message='This file already exists! Do you want to overwrite it?',
1326
+ title='Error',
1327
+ icon='warning',
1328
+ option_1='Yes',
1329
+ option_2='No'
1330
+ )
1331
+ if opts.get() == 'No':
1332
+ return
1333
+
1334
+ self.selected_path = path
1335
+ self.master.destroy()
1336
+ return
1337
+
1338
+ elif self.method in ['askopenfiles', 'askopenfilenames']:
1339
+ selected_items = self.tree.selection()
1340
+ selected_paths = [
1341
+ self.absolute_paths[self.tree.index(item)]
1342
+ for item in selected_items
1343
+ if os.path.isfile(self.absolute_paths[self.tree.index(item)])
1344
+ ]
1345
+
1346
+ if selected_paths:
1347
+ self.selected_paths = selected_paths
1348
+ self.master.destroy()
1349
+ return
1350
+
1351
+ elif self.method in ['askopenfilename', 'askopenfile', 'askdirectory']:
1352
+ if not self.selected_item:
1353
+ return
1354
+
1355
+ if self.method == 'askdirectory' and os.path.isdir(self.selected_item):
1356
+ self.selected_path = self.selected_item
1357
+ self.master.destroy()
1358
+ return
1359
+
1360
+ elif self.method in ['askopenfilename', 'askopenfile'] and os.path.isfile(self.selected_item):
1361
+ self.selected_path = self.selected_item
1362
+ self.master.destroy()
1363
+ return
1364
+
1365
+ def _on_select_item(self, event=None):
1366
+ selected_item = self.tree.focus()
1367
+ items = self.tree.get_children()
1368
+
1369
+ if not selected_item or not items:
1370
+ return
1371
+
1372
+ try:
1373
+ idx = items.index(selected_item)
1374
+ if hasattr(self, 'absolute_paths') and idx < len(self.absolute_paths):
1375
+ self.selected_item = self.absolute_paths[idx]
1376
+ except (ValueError, IndexError):
1377
+ pass
1378
+
1379
+ def _on_click(self, event=None):
1380
+ selected_item = self.tree.focus()
1381
+ items = self.tree.get_children()
1382
+
1383
+ if not selected_item:
1384
+ return
1385
+
1386
+ idx = items.index(selected_item)
1387
+ self.selected_item = self.absolute_paths[idx]
1388
+
1389
+ if os.path.isdir(self.selected_item):
1390
+ self.initial_dir = self.selected_item
1391
+ self.path_entry.delete(0, ctk.END)
1392
+ self.path_entry.insert(0, self.selected_item)
1393
+ self.list_files()
1394
+ return
1395
+
1396
+ # If it's a file:
1397
+ self.path_entry.delete(0, ctk.END)
1398
+ self.path_entry.insert(0, self.selected_item)
1399
+
1400
+
1401
+ def _up(self):
1402
+ current_path = os.path.abspath(os.path.expandvars(os.path.expanduser(self.initial_dir)))
1403
+
1404
+ self.initial_dir = os.path.dirname(current_path)
1405
+
1406
+ self.path_entry.delete(0, ctk.END)
1407
+ self.path_entry.insert(0, self.initial_dir)
1408
+
1409
+ self.list_files()