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,1013 @@
1
+ #!/usr/bin/env python
2
+ """Default (full) file dialog — advanced explorer UI.
3
+
4
+ Business logic (listing, sorting, search, media) lives in core/ and preview/.
5
+ This module only builds widgets and wires events.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any, List, Optional
14
+
15
+ import _tkinter
16
+ import customtkinter as ctk
17
+ from CTkMessagebox import CTkMessagebox
18
+ from PIL import Image
19
+
20
+ from ..core.filesystem import get_file_info, list_directory, prompt_create_folder
21
+ from ..core.search import filter_by_query
22
+ from ..core.sorting import sort_files
23
+ from ..preview.media import get_video_frame, is_image, is_video, thumbnail_image
24
+ from ..resources.icons import icon_for_extension, load_default_icons
25
+ from ..system.platform import System
26
+ from ..utils.helpers import fix_name
27
+ from .tooltip import CustomToolTip
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Constants
31
+ # ---------------------------------------------------------------------------
32
+ BATCH_SIZE = 50
33
+ GRID_COLUMNS = 5
34
+ ICON_THUMB_SIZE = (32, 32)
35
+ BUTTON_WIDTH = 180
36
+ BUTTON_HEIGHT = 60
37
+
38
+
39
+ class DrawApp:
40
+ """Full-featured file dialog (Default style).
41
+
42
+ Public attribute contract (used by ``_functions``):
43
+ selected_file : str
44
+ selected_objects : list
45
+ app : CTkToplevel
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ method: str,
51
+ filetypes: Optional[List[str]] = None,
52
+ bufering: int = 1,
53
+ encoding: str = "utf-8",
54
+ current_path: str = ".",
55
+ hidden: bool = False,
56
+ preview_img: bool = False,
57
+ autocomplete: bool = False,
58
+ video_preview: bool = False,
59
+ tool_tip: bool = False,
60
+ foldercreation: bool = True,
61
+ title: str = "CTkFileDialog",
62
+ geometry: str = "1320x720",
63
+ ) -> None:
64
+ self.foldercreation = foldercreation
65
+ self.current_path = current_path
66
+ if not self.current_path:
67
+ self.current_path = os.getcwd()
68
+ else:
69
+ self.current_path = System.parse_path(path=self.current_path)
70
+
71
+ self.autocomplete = autocomplete
72
+ self.preview_img = preview_img
73
+ self.bufering = bufering
74
+ self.encoding = encoding
75
+ self.hidden = hidden
76
+ self.video_preview = video_preview
77
+ self.suggest: list = []
78
+ self.tool_tip = tool_tip
79
+ self._all_buttons: list = []
80
+ self._icon_cache: dict[str, ctk.CTkImage] = {}
81
+ self.filetypes = filetypes
82
+ self.tab_index = -1
83
+ self.method = method
84
+ self.current_theme = ctk.get_appearance_mode()
85
+ self.view_mode = "grid"
86
+ self.display_files: list = []
87
+ self.BATCH = BATCH_SIZE
88
+ self.selected_file = ""
89
+ self.selected_objects: list = []
90
+ self._temp_item = None
91
+ self._temp_items: list = []
92
+ self._selected_row_frames: list = []
93
+ self.LOADED = 0
94
+ self.files: list = []
95
+ self.entire_paths: list | None = None
96
+
97
+ self.icons = load_default_icons()
98
+
99
+ self.app = ctk.CTkToplevel()
100
+ self.app.title(string=title)
101
+ self.app.geometry(geometry)
102
+ self.app.protocol("WM_DELETE_WINDOW", self.protocol_windows)
103
+
104
+ self._build_top_bar()
105
+ self._build_left_side()
106
+ self._build_center()
107
+ try:
108
+ self.app.grab_set()
109
+ except _tkinter.TclError:
110
+ pass
111
+
112
+ # ------------------------------------------------------------------
113
+ # Lifecycle
114
+ # ------------------------------------------------------------------
115
+
116
+ def protocol_windows(self) -> None:
117
+ try:
118
+ self.app.destroy()
119
+ self.app.unbind_all("<MouseWheel>")
120
+ except Exception:
121
+ pass
122
+
123
+ def close_app(self) -> None:
124
+ if self.method in ("asksaveasfilename", "asksaveasfile"):
125
+ if not os.path.isdir(self.PathEntry.get()):
126
+ self.selected_file = self.PathEntry.get()
127
+ self.protocol_windows()
128
+ self.app.destroy()
129
+ return
130
+
131
+ if self._temp_item:
132
+ self.protocol_windows()
133
+ self.app.destroy()
134
+ if self.method == "asksaveasfile":
135
+ self.selected_file = self._temp_item
136
+ return
137
+ if self.method == "askopenfile":
138
+ self.selected_file = self._temp_item
139
+ else:
140
+ self.selected_file = self._temp_item
141
+ return
142
+
143
+ if len(self._temp_items) >= 1:
144
+ self.protocol_windows()
145
+ self.app.destroy()
146
+ if self.method in ("askopenfilenames", "askopenfiles"):
147
+ seen: set = set()
148
+ self.selected_objects = [
149
+ f
150
+ for f in self._temp_items
151
+ if not os.path.isdir(f) and f not in seen and not seen.add(f)
152
+ ]
153
+ return
154
+ if self.method == "askdirectories":
155
+ seen = set()
156
+ self.selected_objects = [
157
+ f
158
+ for f in self._temp_items
159
+ if os.path.isdir(f) and f not in seen and not seen.add(f)
160
+ ]
161
+ return
162
+ if self.method == "askopenpathnames":
163
+ seen = set()
164
+ self.selected_objects = [
165
+ f for f in self._temp_items if f not in seen and not seen.add(f)
166
+ ]
167
+ return
168
+
169
+ # ------------------------------------------------------------------
170
+ # Navigation
171
+ # ------------------------------------------------------------------
172
+
173
+ def update_entry(self, path) -> None:
174
+ self.PathEntry.configure(state="normal")
175
+ self.PathEntry.delete(0, "end")
176
+ self.PathEntry.insert(0, path)
177
+
178
+ def btn_back(self, master: ctk.CTkToplevel) -> None:
179
+ if self.current_path != os.path.dirname(self.current_path):
180
+ self.current_path = os.path.dirname(self.current_path)
181
+ self.update_entry(path=self.current_path)
182
+ self._list_files(master)
183
+
184
+ def navigate_to(self, path: str, master) -> None:
185
+ try:
186
+ path = os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
187
+
188
+ if os.path.isdir(path):
189
+ if self.method in ("askdirectory", "askopenpathname"):
190
+ self._temp_item = path
191
+ self.current_path = Path(path)
192
+ self.update_entry(path=self.current_path)
193
+ self._list_files(master)
194
+ return
195
+
196
+ if self.method in ("asksaveasfile", "asksaveasfilename"):
197
+ if os.path.isfile(path):
198
+ msg = CTkMessagebox(
199
+ message="This file exists. Do you want to overwrite it?",
200
+ icon="warning",
201
+ title="Warning",
202
+ option_1="Yes",
203
+ option_2="No",
204
+ )
205
+ if msg.get() == "No":
206
+ return
207
+ self._temp_item = path
208
+ self.close_app()
209
+ return
210
+
211
+ if self.method == "askopenfile":
212
+ if not os.path.isfile(path):
213
+ CTkMessagebox(
214
+ message="File not found!", title="Error", icon="cancel"
215
+ )
216
+ self.PathEntry.delete(0, ctk.END)
217
+ self.PathEntry.insert(0, self.current_path)
218
+ return
219
+ self._temp_item = path
220
+ self.update_entry(self._temp_item)
221
+ return
222
+
223
+ if os.path.isfile(path):
224
+ self._temp_item = path
225
+ self.update_entry(self._temp_item)
226
+ return
227
+
228
+ self.PathEntry.delete(0, "end")
229
+ self.PathEntry.insert(0, str(self.current_path))
230
+ self.PathEntry.configure(state="normal")
231
+ CTkMessagebox(
232
+ message="No such file or directory!", title="Error", icon="cancel"
233
+ )
234
+ except PermissionError:
235
+ CTkMessagebox(message="Permission denied!", title="Error", icon="cancel")
236
+ except FileNotFoundError:
237
+ CTkMessagebox(message="File Not Found!", title="Error", icon="cancel")
238
+
239
+ def _create_new_folder(self) -> None:
240
+ if not self.foldercreation:
241
+ return
242
+ prompt_create_folder(
243
+ str(self.current_path),
244
+ on_success=lambda: self._list_files(master=self.app),
245
+ )
246
+
247
+ # ------------------------------------------------------------------
248
+ # Autocomplete
249
+ # ------------------------------------------------------------------
250
+
251
+ def _autocomplete(self, event) -> str:
252
+ if not hasattr(self, "entire_paths") or not self.entire_paths:
253
+ return "break"
254
+ if not self.files:
255
+ return "break"
256
+
257
+ max_index = len(self.files)
258
+ if event.keysym == "Up":
259
+ self.tab_index = (self.tab_index - 1) % max_index
260
+ else:
261
+ self.tab_index = (self.tab_index + 1) % max_index
262
+
263
+ path = self.entire_paths[self.tab_index]
264
+ self.PathEntry.delete(0, ctk.END)
265
+ self.PathEntry.insert(0, path)
266
+ self._temp_item = path
267
+ return "break"
268
+
269
+ # ------------------------------------------------------------------
270
+ # UI construction
271
+ # ------------------------------------------------------------------
272
+
273
+ def _build_top_bar(self) -> None:
274
+ master = self.app
275
+ top = ctk.CTkFrame(master=master, height=40, fg_color="transparent")
276
+ top.pack(side="top", fill="x")
277
+
278
+ def btn_exit():
279
+ msg = CTkMessagebox(
280
+ message="Do you want to exit?",
281
+ title="Exit",
282
+ option_1="Yes",
283
+ option_2="No",
284
+ icon="warning",
285
+ )
286
+ if msg.get() == "Yes":
287
+ self.protocol_windows()
288
+ self.selected_file = None
289
+ self.selected_objects = []
290
+ self._temp_item = None
291
+ self._temp_items = []
292
+ master.destroy()
293
+
294
+ ctk.CTkButton(
295
+ master=top,
296
+ text="Exit",
297
+ font=("Hack Nerd Font", 15),
298
+ width=70,
299
+ command=btn_exit,
300
+ hover_color="red",
301
+ ).pack(side="left", fill="x")
302
+
303
+ self.PathEntry = ctk.CTkEntry(
304
+ master=top, width=1070, corner_radius=0, insertwidth=0
305
+ )
306
+ self.PathEntry.insert(index=0, string=System.get_path(str(self.current_path)))
307
+ self.PathEntry.pack(side="right", fill="y", padx=10, pady=10)
308
+ self.PathEntry.bind(
309
+ "<Return>",
310
+ lambda e: self.navigate_to(
311
+ path=self.PathEntry.get(), master=master
312
+ ),
313
+ )
314
+ self.PathEntry.bind("<Alt-Left>", lambda e: self.btn_back(master=master))
315
+
316
+ ctk.CTkButton(
317
+ master=top,
318
+ text="",
319
+ font=("Hack Nerd Font", 15),
320
+ width=70,
321
+ command=lambda: self.btn_back(master=master),
322
+ ).pack(side="left", fill="x", padx=10, pady=10)
323
+
324
+ ctk.CTkButton(
325
+ master=top,
326
+ text="Ok",
327
+ font=("Hack Nerd Font", 15),
328
+ width=70,
329
+ command=lambda: self.close_app(),
330
+ ).pack(side="left", fill="x", padx=10, pady=10)
331
+
332
+ if self.foldercreation:
333
+ ctk.CTkButton(
334
+ master=top,
335
+ text="New Folder",
336
+ font=("Hack Nerd Font", 15),
337
+ width=100,
338
+ command=self._create_new_folder,
339
+ ).pack(side="left", fill="x", padx=10, pady=10)
340
+
341
+ if self.autocomplete:
342
+ for key in ("<Down>", "<Up>", "<Tab>"):
343
+ self.PathEntry.bind(key, self._autocomplete)
344
+
345
+ self.app.bind_all("<Alt-Left>", lambda e: self.btn_back(master=self.app))
346
+
347
+ # Search + view + sort bar
348
+ search_frame = ctk.CTkFrame(master=master, fg_color="transparent", height=40)
349
+ search_frame.pack(side="top", fill="x", padx=10, pady=(5, 10))
350
+ self.SearchFrame = search_frame
351
+
352
+ ctk.CTkLabel(search_frame, text="Search:", font=("Arial", 12)).pack(
353
+ side="left", padx=(0, 10)
354
+ )
355
+ self.SearchEntry = ctk.CTkEntry(
356
+ search_frame, placeholder_text="Type to search files..."
357
+ )
358
+ self.SearchEntry.pack(expand=True, fill="x", side="left", padx=(0, 20))
359
+ self.SearchEntry.bind("<KeyRelease>", lambda _: self._search_files())
360
+
361
+ ctk.CTkLabel(search_frame, text="View:", font=("Arial", 12)).pack(
362
+ side="left", padx=(0, 10)
363
+ )
364
+ self.grid_btn = ctk.CTkButton(
365
+ search_frame,
366
+ text="📊 Grid",
367
+ width=60,
368
+ command=lambda: self._set_view_mode("grid"),
369
+ )
370
+ self.grid_btn.pack(side="left", padx=5)
371
+ self.list_btn = ctk.CTkButton(
372
+ search_frame,
373
+ text="📋 List",
374
+ width=60,
375
+ command=lambda: self._set_view_mode("list"),
376
+ )
377
+ self.list_btn.pack(side="left", padx=5)
378
+
379
+ ctk.CTkLabel(search_frame, text="Sort:", font=("Arial", 12)).pack(
380
+ side="left", padx=(20, 10)
381
+ )
382
+ self.sort_var = ctk.StringVar(value="name")
383
+ self.sort_menu = ctk.CTkOptionMenu(
384
+ search_frame,
385
+ values=["name", "date", "type", "size", "modified"],
386
+ command=self._on_sort_change,
387
+ variable=self.sort_var,
388
+ )
389
+ self.sort_menu.pack(side="left", padx=5)
390
+
391
+ def _build_left_side(self) -> None:
392
+ master = self.app
393
+ left = ctk.CTkFrame(master=master, width=200)
394
+ left.pack(side="left", fill="y", padx=10, pady=10)
395
+ left.pack_propagate(False)
396
+
397
+ home = os.path.expanduser("~")
398
+ folders = {f"{str(os.getenv('HOME')).replace('/home/', '')}": home}
399
+
400
+ dir_file = os.path.join(home, ".config/user-dirs.dirs")
401
+ pattern = re.compile(r'XDG_\w+_DIR="(.+?)"')
402
+
403
+ import platform
404
+
405
+ if platform.system() == "Linux":
406
+ if not os.path.exists(path=dir_file):
407
+ raise FileNotFoundError(
408
+ f"The file {dir_file} is required for the program to run!"
409
+ )
410
+ with open(dir_file, "r") as f:
411
+ for line in f:
412
+ if not line.startswith("#") and line.strip():
413
+ match = pattern.search(line)
414
+ if match:
415
+ path = os.path.expandvars(match.group(1))
416
+ name = os.path.basename(os.path.normpath(path))
417
+ if name != f"{os.getenv('USER')}":
418
+ folders[name] = path
419
+ elif platform.system() == "Windows":
420
+ home_p = Path.home()
421
+ win_folders = {
422
+ home_p.name: str(home_p),
423
+ "Desktop": home_p / "Desktop",
424
+ "Documents": home_p / "Documents",
425
+ "Downloads": home_p / "Downloads",
426
+ "Pictures": home_p / "Pictures",
427
+ "Music": home_p / "Music",
428
+ "Videos": home_p / "Videos",
429
+ }
430
+ folders = {k: v for k, v in win_folders.items()}
431
+
432
+ ctk.CTkLabel(
433
+ master=left, text="Places", font=("Hack Nerd Font", 15)
434
+ ).pack(side=ctk.TOP, padx=5, pady=5)
435
+
436
+ icons_map = {
437
+ os.getenv("USER"): "",
438
+ "Desktop": "",
439
+ "Downloads": "",
440
+ "Documents": "",
441
+ "Pictures": "",
442
+ "Music": "",
443
+ "Videos": "",
444
+ "Templates": "",
445
+ "Public": "",
446
+ }
447
+
448
+ for name, path in folders.items():
449
+ icon = icons_map.get(name, "")
450
+ ctk.CTkButton(
451
+ master=left,
452
+ text=f" {icon} {name}",
453
+ font=("Hack Nerd Font", 14),
454
+ anchor="w",
455
+ fg_color="transparent",
456
+ hover_color="#8da3ae",
457
+ text_color=(
458
+ "#000000"
459
+ if self.current_theme.lower() == "light"
460
+ else "#cccccc"
461
+ ),
462
+ corner_radius=2,
463
+ border_width=0,
464
+ command=lambda r=path: self.navigate_to(path=r, master=master),
465
+ ).pack(fill="x", pady=4)
466
+
467
+ def _build_center(self) -> None:
468
+ master = self.app
469
+ self.CenterSideFrame = ctk.CTkScrollableFrame(master=master)
470
+ self.CenterSideFrame.pack(
471
+ expand=True, side="top", fill="both", padx=10, pady=10
472
+ )
473
+ self._bind_scroll()
474
+ self.content_frame = ctk.CTkFrame(master=self.CenterSideFrame)
475
+ self.content_frame.pack(
476
+ side="top", fill="both", expand=True, padx=20, pady=10
477
+ )
478
+ self.content_frame.grid_columnconfigure(0, weight=1)
479
+ self._list_files(master=master)
480
+
481
+ def _bind_scroll(self) -> None:
482
+ canvas = self.CenterSideFrame._parent_canvas
483
+
484
+ def _on_mousewheel(event):
485
+ try:
486
+ x_root = getattr(event, "x_root", None)
487
+ y_root = getattr(event, "y_root", None)
488
+ if x_root is not None and y_root is not None:
489
+ x1 = self.CenterSideFrame.winfo_rootx()
490
+ y1 = self.CenterSideFrame.winfo_rooty()
491
+ x2 = x1 + self.CenterSideFrame.winfo_width()
492
+ y2 = y1 + self.CenterSideFrame.winfo_height()
493
+ if not (x1 <= x_root <= x2 and y1 <= y_root <= y2):
494
+ return
495
+
496
+ if hasattr(event, "num"):
497
+ if event.num == 4:
498
+ canvas.yview_scroll(-1, "units")
499
+ self._check_scroll(self.app)
500
+ return "break"
501
+ if event.num == 5:
502
+ canvas.yview_scroll(1, "units")
503
+ self._check_scroll(self.app)
504
+ return "break"
505
+ if hasattr(event, "delta"):
506
+ canvas.yview_scroll(-int(event.delta / 120), "units")
507
+ self._check_scroll(self.app)
508
+ return "break"
509
+ except Exception:
510
+ pass
511
+
512
+ self.app.bind_all("<MouseWheel>", _on_mousewheel)
513
+ self.app.bind_all("<Button-4>", _on_mousewheel)
514
+ self.app.bind_all("<Button-5>", _on_mousewheel)
515
+ for widget in canvas.winfo_children():
516
+ widget.bind("<MouseWheel>", _on_mousewheel)
517
+ widget.bind("<Button-4>", _on_mousewheel)
518
+ widget.bind("<Button-5>", _on_mousewheel)
519
+
520
+ # ------------------------------------------------------------------
521
+ # File listing / display
522
+ # ------------------------------------------------------------------
523
+
524
+ def __clear__(self) -> None:
525
+ for widget in self.content_frame.winfo_children():
526
+ try:
527
+ widget.destroy()
528
+ except (_tkinter.TclError, Exception):
529
+ pass
530
+ self._selected_row_frames.clear()
531
+ self._icon_cache.clear()
532
+
533
+ def _list_files(self, master: ctk.CTkToplevel) -> None:
534
+ self.LOADED = 0
535
+ self.BATCH = BATCH_SIZE
536
+ self.selected_objects.clear()
537
+ self._all_buttons.clear()
538
+ self.CenterSideFrame._parent_canvas.yview_moveto(0)
539
+ self.__clear__()
540
+
541
+ path = self.current_path
542
+ try:
543
+ self.files = list_directory(
544
+ str(path),
545
+ method=self.method,
546
+ hidden=self.hidden,
547
+ filetypes=self.filetypes,
548
+ )
549
+ except (PermissionError, FileNotFoundError, OSError):
550
+ self.files = []
551
+ self.display_files = []
552
+ return
553
+
554
+ if not self.files:
555
+ self.display_files = []
556
+ return
557
+
558
+ if self.autocomplete:
559
+ self.entire_paths = [
560
+ os.path.join(self.current_path, f) for f in self.files
561
+ ] or None
562
+
563
+ sorted_files = sort_files(
564
+ self.files, str(self.current_path), self.sort_var.get()
565
+ )
566
+ self._display_files(sorted_files)
567
+
568
+ def _display_files(self, files: list) -> None:
569
+ self.display_files = files
570
+ self.LOADED = 0
571
+ total = len(files)
572
+ if total <= 0:
573
+ return
574
+ if self.view_mode == "grid":
575
+ self._load_grid_files(total)
576
+ else:
577
+ self._load_list_files(total)
578
+
579
+ def _resolve_icon(self, full_path: str, filename: str):
580
+ cached = self._icon_cache.get(full_path)
581
+ if cached is not None:
582
+ return cached
583
+
584
+ if os.path.isdir(full_path):
585
+ icon = self.icons["folder"]
586
+ self._icon_cache[full_path] = icon
587
+ return icon
588
+
589
+ if self.preview_img and is_image(full_path):
590
+ img = thumbnail_image(full_path, ICON_THUMB_SIZE)
591
+ if img is not None:
592
+ icon = ctk.CTkImage(
593
+ light_image=img, dark_image=img, size=ICON_THUMB_SIZE
594
+ )
595
+ self._icon_cache[full_path] = icon
596
+ return icon
597
+ icon = self.icons.get("image", self.icons["default"])
598
+ self._icon_cache[full_path] = icon
599
+ return icon
600
+
601
+ if self.video_preview and is_video(full_path):
602
+ frame = get_video_frame(full_path, frame_number=10)
603
+ if frame is not None:
604
+ frame.thumbnail(ICON_THUMB_SIZE)
605
+ icon = ctk.CTkImage(
606
+ light_image=frame, dark_image=frame, size=ICON_THUMB_SIZE
607
+ )
608
+ self._icon_cache[full_path] = icon
609
+ return icon
610
+ icon = self.icons.get("video", self.icons["default"])
611
+ self._icon_cache[full_path] = icon
612
+ return icon
613
+
614
+ ext = os.path.splitext(filename)[1].lower()
615
+ icon = icon_for_extension(ext, self.icons)
616
+ self._icon_cache[full_path] = icon
617
+ return icon
618
+
619
+ def _text_color(self) -> str:
620
+ return (
621
+ "#000000" if self.current_theme.lower() == "light" else "#cccccc"
622
+ )
623
+
624
+ def _make_grid_button(
625
+ self,
626
+ full_path: str,
627
+ filename: str,
628
+ file_type: str,
629
+ size_str: str,
630
+ master,
631
+ ):
632
+ icon = self._resolve_icon(full_path, filename)
633
+ fixed_name = fix_name(name=filename)
634
+ command = None
635
+ if self.method not in (
636
+ "askopenfilenames",
637
+ "askdirectories",
638
+ "askopenpathnames",
639
+ ):
640
+ command = lambda r=full_path: self.navigate_to(path=r, master=master)
641
+
642
+ button_text = f"{fixed_name}\n{file_type} • {size_str}"
643
+ boton = ctk.CTkButton(
644
+ master=self.content_frame,
645
+ text=button_text,
646
+ image=icon,
647
+ compound="top",
648
+ width=BUTTON_WIDTH,
649
+ height=100,
650
+ anchor="center",
651
+ fg_color="transparent",
652
+ hover_color="#8da3ae",
653
+ text_color=self._text_color(),
654
+ command=command,
655
+ )
656
+ if self.tool_tip:
657
+ CustomToolTip(widget=boton, message=get_file_info(full_path))
658
+ if self.method in (
659
+ "askopenfilenames",
660
+ "askopenfiles",
661
+ "askdirectories",
662
+ "askopenpathnames",
663
+ ):
664
+ boton.bind(
665
+ "<Button-1>",
666
+ lambda event, r=full_path, b=boton: self._handle_click(
667
+ event, r, master, b
668
+ ),
669
+ )
670
+ return boton
671
+
672
+ def _load_grid_files(self, cantidad: int) -> None:
673
+ while self.LOADED < len(self.display_files) and cantidad > 0:
674
+ file = self.display_files[self.LOADED]
675
+ full_path = os.path.join(self.current_path, file)
676
+ if self.method in ("askdirectory", "askdirectories") and os.path.isfile(
677
+ full_path
678
+ ):
679
+ self.LOADED += 1
680
+ continue
681
+
682
+ try:
683
+ st = os.stat(full_path)
684
+ file_size = st.st_size
685
+ except OSError:
686
+ file_size = 0
687
+
688
+ is_dir = os.path.isdir(full_path)
689
+ file_type = (
690
+ "Directory"
691
+ if is_dir
692
+ else os.path.splitext(file)[1][1:].upper() or "File"
693
+ )
694
+
695
+ if file_size < 1024:
696
+ size_str = f"{file_size} B"
697
+ elif file_size < 1024 * 1024:
698
+ size_str = f"{file_size / 1024:.1f} KB"
699
+ else:
700
+ size_str = f"{file_size / (1024 * 1024):.1f} MB"
701
+
702
+ row = self.LOADED // GRID_COLUMNS
703
+ col = self.LOADED % GRID_COLUMNS
704
+ boton = self._make_grid_button(
705
+ full_path,
706
+ file,
707
+ file_type,
708
+ size_str,
709
+ self.app,
710
+ )
711
+ boton.grid(row=row, column=col, padx=10, pady=10)
712
+ self.LOADED += 1
713
+ cantidad -= 1
714
+
715
+ try:
716
+ self.content_frame.update_idletasks()
717
+ self.CenterSideFrame._parent_canvas.configure(
718
+ scrollregion=self.CenterSideFrame._parent_canvas.bbox("all")
719
+ )
720
+ except Exception:
721
+ pass
722
+
723
+ def _load_list_files(self, cantidad: int) -> None:
724
+ if self.LOADED == 0:
725
+ header_bg = "#343638" if self.current_theme.lower() == "dark" else "#f0f0f0"
726
+ header_fg = "#ffffff" if self.current_theme.lower() == "dark" else "#000000"
727
+ header_frame = ctk.CTkFrame(
728
+ self.content_frame,
729
+ fg_color=header_bg,
730
+ corner_radius=10,
731
+ height=40,
732
+ )
733
+ header_frame.pack(fill="x", padx=10, pady=(0, 8))
734
+ header_frame.grid_columnconfigure(0, minsize=56)
735
+ header_frame.grid_columnconfigure(1, weight=1)
736
+ header_frame.grid_columnconfigure(2, minsize=80)
737
+ header_frame.grid_columnconfigure(3, minsize=80)
738
+ header_frame.grid_columnconfigure(4, minsize=120)
739
+ header_frame.grid_rowconfigure(0, minsize=40)
740
+
741
+ ctk.CTkLabel(
742
+ master=header_frame,
743
+ text="",
744
+ width=56,
745
+ anchor="w",
746
+ text_color=header_fg,
747
+ font=("Arial", 11, "bold"),
748
+ ).grid(row=0, column=0, padx=12, pady=8, sticky="w")
749
+ ctk.CTkLabel(
750
+ master=header_frame,
751
+ text="Name",
752
+ anchor="w",
753
+ text_color=header_fg,
754
+ font=("Arial", 11, "bold"),
755
+ ).grid(row=0, column=1, padx=4, pady=8, sticky="ew")
756
+ ctk.CTkLabel(
757
+ master=header_frame,
758
+ text="Type",
759
+ anchor="w",
760
+ text_color=header_fg,
761
+ font=("Arial", 11, "bold"),
762
+ ).grid(row=0, column=2, padx=4, pady=8, sticky="w")
763
+ ctk.CTkLabel(
764
+ master=header_frame,
765
+ text="Size",
766
+ anchor="w",
767
+ text_color=header_fg,
768
+ font=("Arial", 11, "bold"),
769
+ ).grid(row=0, column=3, padx=4, pady=8, sticky="w")
770
+ ctk.CTkLabel(
771
+ master=header_frame,
772
+ text="Modified",
773
+ anchor="w",
774
+ text_color=header_fg,
775
+ font=("Arial", 11, "bold"),
776
+ ).grid(row=0, column=4, padx=4, pady=8, sticky="e")
777
+
778
+ while self.LOADED < len(self.display_files) and cantidad > 0:
779
+ file = self.display_files[self.LOADED]
780
+ full_path = os.path.join(self.current_path, file)
781
+ if self.method in ("askdirectory", "askdirectories") and os.path.isfile(
782
+ full_path
783
+ ):
784
+ self.LOADED += 1
785
+ continue
786
+
787
+ try:
788
+ st = os.stat(full_path)
789
+ file_size = st.st_size
790
+ mod_time = time.strftime(
791
+ "%Y-%m-%d %H:%M", time.localtime(st.st_mtime)
792
+ )
793
+ except OSError:
794
+ file_size = 0
795
+ mod_time = "N/A"
796
+
797
+ is_dir = os.path.isdir(full_path)
798
+ file_type = (
799
+ "Directory"
800
+ if is_dir
801
+ else os.path.splitext(file)[1][1:].upper() or "File"
802
+ )
803
+ icon = self._resolve_icon(full_path, file)
804
+
805
+ if file_size < 1024:
806
+ size_str = f"{file_size} B"
807
+ elif file_size < 1024 * 1024:
808
+ size_str = f"{file_size / 1024:.1f} KB"
809
+ else:
810
+ size_str = f"{file_size / (1024 * 1024):.1f} MB"
811
+
812
+ row_bg = "#2f3136" if self.current_theme.lower() == "dark" else "#f7f7f7"
813
+ item_frame = ctk.CTkFrame(
814
+ self.content_frame,
815
+ fg_color=row_bg,
816
+ corner_radius=12,
817
+ )
818
+ item_frame.pack(fill="x", padx=10, pady=4, ipady=8)
819
+
820
+ ctk.CTkLabel(
821
+ master=item_frame,
822
+ image=icon,
823
+ text="",
824
+ width=56,
825
+ anchor="w",
826
+ ).pack(side="left", padx=(12, 4), pady=8)
827
+
828
+ ctk.CTkLabel(
829
+ master=item_frame,
830
+ text=file,
831
+ anchor="w",
832
+ text_color=self._text_color(),
833
+ font=("Arial", 11, "bold"),
834
+ ).pack(side="left", fill="x", expand=True, padx=4, pady=8)
835
+
836
+ ctk.CTkLabel(
837
+ master=item_frame,
838
+ text=file_type,
839
+ anchor="w",
840
+ text_color="#a6a6a6" if self.current_theme.lower() == "dark" else "#606060",
841
+ font=("Arial", 10),
842
+ width=80,
843
+ ).pack(side="left", padx=4, pady=8)
844
+
845
+ ctk.CTkLabel(
846
+ master=item_frame,
847
+ text=size_str,
848
+ anchor="w",
849
+ text_color="#a6a6a6" if self.current_theme.lower() == "dark" else "#606060",
850
+ font=("Arial", 10),
851
+ width=80,
852
+ ).pack(side="left", padx=4, pady=8)
853
+
854
+ date_label = ctk.CTkLabel(
855
+ master=item_frame,
856
+ text=mod_time,
857
+ anchor="e",
858
+ text_color=self._text_color(),
859
+ font=("Arial", 10),
860
+ width=120,
861
+ )
862
+ date_label.pack(side="left", padx=(4, 12), pady=8)
863
+
864
+ command = None
865
+ if self.method not in (
866
+ "askopenfilenames",
867
+ "askdirectories",
868
+ "askopenpathnames",
869
+ ):
870
+ command = lambda r=full_path: self.navigate_to(
871
+ path=r, master=self.app
872
+ )
873
+
874
+ if self.method in (
875
+ "askopenfilenames",
876
+ "askopenfiles",
877
+ "askdirectories",
878
+ "askopenpathnames",
879
+ ):
880
+ self._selected_row_frames.append(item_frame)
881
+ def row_callback(event, r=full_path, f=item_frame):
882
+ self._select_list_row(event, r, f)
883
+ elif command is not None:
884
+ def row_callback(event, r=full_path):
885
+ command(r)
886
+ else:
887
+ row_callback = None
888
+
889
+ if row_callback is not None:
890
+ item_frame.bind("<Button-1>", row_callback)
891
+ for child in item_frame.winfo_children():
892
+ child.bind("<Button-1>", row_callback)
893
+
894
+ self.LOADED += 1
895
+ cantidad -= 1
896
+
897
+ try:
898
+ self.content_frame.update_idletasks()
899
+ self.CenterSideFrame._parent_canvas.configure(
900
+ scrollregion=self.CenterSideFrame._parent_canvas.bbox("all")
901
+ )
902
+ except Exception:
903
+ pass
904
+
905
+ def _check_scroll(self, master) -> None:
906
+ try:
907
+ canvas = self.CenterSideFrame._parent_canvas
908
+ yview = canvas.yview()
909
+ if not hasattr(self, "display_files") or not self.display_files:
910
+ return
911
+ if yview[1] > 0.80 and self.LOADED < len(self.display_files):
912
+ if self.view_mode == "grid":
913
+ self._load_grid_files(self.BATCH)
914
+ else:
915
+ self._load_list_files(self.BATCH)
916
+ except _tkinter.TclError:
917
+ pass
918
+
919
+ def _search_files(self) -> None:
920
+ if not hasattr(self, "files") or not self.files:
921
+ return
922
+ query = self.SearchEntry.get()
923
+ self.__clear__()
924
+ if not query:
925
+ self._list_files(self.app)
926
+ return
927
+ filtered = filter_by_query(self.files, query)
928
+ sorted_filtered = sort_files(
929
+ filtered, str(self.current_path), self.sort_var.get()
930
+ )
931
+ self._display_files(sorted_filtered)
932
+
933
+ def _set_view_mode(self, mode: str) -> None:
934
+ self.view_mode = mode
935
+ if mode == "grid":
936
+ self.grid_btn.configure(fg_color="blue")
937
+ self.list_btn.configure(fg_color="gray30")
938
+ else:
939
+ self.grid_btn.configure(fg_color="gray30")
940
+ self.list_btn.configure(fg_color="blue")
941
+ self._list_files(self.app)
942
+
943
+ def _on_sort_change(self, value) -> None:
944
+ self._list_files(self.app)
945
+
946
+ def _handle_click(self, event, r, master, boton, tool_tip=None) -> None:
947
+ if not event.state & 0x0004:
948
+ self._temp_items.clear()
949
+ self.selected_objects.clear()
950
+
951
+ if event.state & 0x0004:
952
+ if self.method in (
953
+ "askopenfilenames",
954
+ "askopenfiles",
955
+ "askdirectories",
956
+ "askopenpathnames",
957
+ ):
958
+ if r not in self._temp_items:
959
+ self._temp_items.append(r)
960
+ boton.configure(fg_color="blue")
961
+ return
962
+ if boton not in self._all_buttons:
963
+ self._all_buttons.append(boton)
964
+ else:
965
+ self._temp_items.clear()
966
+ if self.method in (
967
+ "askopenfilenames",
968
+ "askopenfiles",
969
+ "askdirectories",
970
+ "askopenpathnames",
971
+ ):
972
+ self._temp_items.append(r)
973
+ for btn in self._all_buttons:
974
+ if btn.winfo_exists():
975
+ btn.configure(
976
+ fg_color="transparent",
977
+ hover_color="#8da3ae",
978
+ text_color=self._text_color(),
979
+ )
980
+ if os.path.isdir(r):
981
+ self.navigate_to(path=r, master=master)
982
+ else:
983
+ self._temp_items.append(r)
984
+
985
+ def _select_list_row(self, event, r, frame) -> None:
986
+ if not event.state & 0x0004:
987
+ self._temp_items.clear()
988
+ self.selected_objects.clear()
989
+ for row in self._selected_row_frames:
990
+ if row.winfo_exists():
991
+ row.configure(
992
+ fg_color="#2f3136"
993
+ if self.current_theme.lower() == "dark"
994
+ else "#f7f7f7"
995
+ )
996
+ self._selected_row_frames.clear()
997
+
998
+ if self.method in (
999
+ "askopenfilenames",
1000
+ "askopenfiles",
1001
+ "askdirectories",
1002
+ "askopenpathnames",
1003
+ ):
1004
+ if r not in self._temp_items:
1005
+ self._temp_items.append(r)
1006
+ if frame not in self._selected_row_frames:
1007
+ self._selected_row_frames.append(frame)
1008
+ frame.configure(fg_color="#2073d4")
1009
+ return
1010
+
1011
+
1012
+ # Backward-compatible alias used by _functions / external code
1013
+ _DrawApp = DrawApp