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.
- CTkFileDialog/Constants/__init__.py +37 -0
- CTkFileDialog/Constants/constants.py +39 -0
- CTkFileDialog/Dialog.py +1409 -0
- CTkFileDialog/__init__.py +27 -0
- CTkFileDialog/_functions.py +371 -0
- CTkFileDialog/_system.py +70 -0
- CTkFileDialog/icons/bash.png +0 -0
- CTkFileDialog/icons/conf.png +0 -0
- CTkFileDialog/icons/css.png +0 -0
- CTkFileDialog/icons/exe.png +0 -0
- CTkFileDialog/icons/folder.png +0 -0
- CTkFileDialog/icons/gz.png +0 -0
- CTkFileDialog/icons/html.png +0 -0
- CTkFileDialog/icons/image.png +0 -0
- CTkFileDialog/icons/ini.png +0 -0
- CTkFileDialog/icons/javascript.png +0 -0
- CTkFileDialog/icons/js.png +0 -0
- CTkFileDialog/icons/json.png +0 -0
- CTkFileDialog/icons/markdown.png +0 -0
- CTkFileDialog/icons/odt.png +0 -0
- CTkFileDialog/icons/pdf.png +0 -0
- CTkFileDialog/icons/php.png +0 -0
- CTkFileDialog/icons/python.png +0 -0
- CTkFileDialog/icons/text.png +0 -0
- CTkFileDialog/icons/video.png +0 -0
- ctkfiledialog_plus-0.3.0.dist-info/METADATA +343 -0
- ctkfiledialog_plus-0.3.0.dist-info/RECORD +29 -0
- ctkfiledialog_plus-0.3.0.dist-info/WHEEL +5 -0
- ctkfiledialog_plus-0.3.0.dist-info/top_level.txt +1 -0
CTkFileDialog/Dialog.py
ADDED
|
@@ -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()
|