-
Notifications
You must be signed in to change notification settings - Fork 0
/
piccull.py
359 lines (306 loc) · 13.7 KB
/
piccull.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
"""
This module implements the PicCull application, a simple GUI tool for image culling.
The PicCull application allows users to navigate through a directory of images
and either delete the image or move it to a separate 'culled' folder. The user can also
apply custom keybindings for certain actions and modify some settings.
The application uses the tkinter library for the graphical user interface and
the PIL (Pillow) library for image handling.
"""
from tkinter import filedialog, StringVar, IntVar
import os
import shutil
import subprocess
import platform
import customtkinter as ctk
from PIL import Image
class PicCull:
"""
The PicCull class encapsulates the behavior of the PicCull application.
This class is responsible for creating and managing the graphical user interface of the application,
including the image display, navigation buttons, utility buttons, and status bar. It also manages
the opening and culling of images from a selected directory, as well as the handling of user-defined
keybindings and settings.
Attributes:
master: The root window for the application.
index: The index of the current image.
image_paths: A list of paths to the images in the currently selected directory.
directory_path: The path to the currently selected directory.
culled_dir: The directory where culled images are moved.
delete_on_cull: A flag indicating whether to delete the images when they're culled.
img_label: The label for displaying images.
btn_open_culled: The button for opening the culled directory.
btn_prev: The button for navigating to the previous image.
btn_cull: The button for culling the current image.
btn_next: The button for navigating to the next image.
keybindings: A dictionary of keybindings for certain actions.
keybindings_entries: A dictionary of entries for keybindings in the settings window.
status_var: A string variable for the status bar.
status_bar: The status bar of the application.
"""
def __init__(self, master):
"""
Initializes an instance of the PicCull application.
:param master: The root window for the application.
"""
self.master = master
self.index = 0
self.image_paths = []
self.directory_path = ""
self.culled_dir = None
self.delete_on_cull = IntVar()
self.img_label = None
self.btn_open_culled = None
self.btn_prev = None
self.btn_cull = None
self.btn_next = None
self.keybindings = None
self.keybindings_entries = None
self.status_var = None
self.status_bar = None
self.init_master(master)
self.create_widgets()
self.create_keybindings()
self.create_status_bar()
def init_master(self, master):
"""
Initialize the main application window with certain attributes and configurations.
:param master: The root window for the application.
"""
self.master = master
self.master.title('PicCull')
root.geometry('600x800')
root.minsize(480, 480)
# root.iconbitmap('icon.ico')
ctk.set_default_color_theme('dark-blue')
def create_widgets(self):
"""
Creates the main widgets of the application which include the image
label and utility buttons.
"""
self.create_image_label()
self.create_util_buttons()
self.create_nav_cull_buttons()
def create_image_label(self):
"""
Creates a label to display the image.
"""
self.img_label = ctk.CTkLabel(self.master, text="No image loaded.")
self.img_label.pack()
def create_util_buttons(self):
"""
Creates the utility buttons (Load Directory, Open Culled Folder,
Settings) for the application.
"""
utilbuttons_frame = self.create_frame(self.master)
self.create_button(utilbuttons_frame, "Load Directory", self.open_directory, 0)
self.btn_open_culled = self.create_button(utilbuttons_frame, "Open Culled Folder", self.open_culled_folder, 1, state='disabled')
self.create_button(utilbuttons_frame, "Settings", self.open_settings, 2)
def create_nav_cull_buttons(self):
"""
Creates the navigation and cull buttons (Prev, Cull, Next) for the
application.
"""
navcullbuttons_frame = self.create_frame(self.master)
self.btn_prev = self.create_button(navcullbuttons_frame, "<- Prev", self.prev_image, 0, state='disabled')
self.btn_cull = self.create_button(navcullbuttons_frame, "Cull", self.cull_image, 1, state='disabled', fg_color='red', hover_color='#8B0000')
self.btn_next = self.create_button(navcullbuttons_frame, "Next ->", self.next_image, 2, state='disabled')
def create_frame(self, master):
"""
Creates a new frame in the application.
:param master: The parent widget.
:return: The newly created frame.
"""
frame = ctk.CTkFrame(master)
frame.pack()
return frame
def create_button(self, master, text, command, column, state='normal', **kwargs):
"""
Creates a new button in the application.
:param master: The parent widget.
:param text: Text to display on the button.
:param command: Function to execute when the button is clicked.
:param column: The column where to place the button in the grid.
:param state: The initial state of the button. Default is 'normal'.
:param kwargs: Additional parameters for the button.
:return: The newly created button.
"""
button = ctk.CTkButton(master, text=text, command=command, border_width=2, state=state, **kwargs)
button.grid(row=0, column=column, padx=10, pady=10)
return button
def create_keybindings(self):
"""
Creates keybindings for certain actions in the application.
"""
self.keybindings = {"prev_image": "<Left>", "next_image": "<Right>", "cull_image": "<Down>"}
self.keybindings_entries = {}
for action in self.keybindings:
self.master.bind(self.keybindings[action], lambda e, action=action: getattr(self, action)())
def create_status_bar(self):
"""
Creates a status bar at the bottom of the application window.
"""
self.status_var = StringVar()
self.status_bar = ctk.CTkLabel(self.master, textvariable=self.status_var, anchor='center', bg_color='gray')
self.status_bar.pack(side='bottom', fill='x')
def get_directory(self):
"""
Opens a file dialog to select a directory.
:return: The path of the selected directory.
"""
directory_path = filedialog.askdirectory(initialdir="/", title="Select a Directory")
if not directory_path:
raise IOError("No directory selected.")
return directory_path
def load_image_paths(self, directory_path):
"""
Loads all image files from a given directory.
:param directory_path: The directory from which to load image files.
:return: List of paths to the image files.
"""
image_paths = list(filter(lambda f: f.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif')), os.listdir(directory_path)))
image_paths = [os.path.join(directory_path, f) for f in image_paths]
if not image_paths:
raise ValueError("No images found in the selected directory.")
return image_paths
def open_directory(self):
"""
Opens a directory and loads all image files from it.
"""
try:
self.index = 0
self.directory_path = self.get_directory()
self.culled_dir = os.path.join(self.directory_path, 'pic-culled')
self.btn_open_culled.configure(state='normal' if os.path.exists(self.culled_dir) else 'disabled')
self.image_paths = self.load_image_paths(self.directory_path)
self.status_var.set(f"Loaded directory: {self.directory_path}. Image {self.index+1}/{len(self.image_paths)}")
self.show_image()
except (IOError, ValueError) as error:
self.status_var.set(str(error))
def open_culled_folder(self):
"""
Opens the folder containing the culled images.
"""
if self.culled_dir and os.path.exists(self.culled_dir):
if platform.system() == 'Windows':
os.startfile(self.culled_dir)
elif platform.system() == 'Darwin':
subprocess.Popen(["open", self.culled_dir])
else:
subprocess.Popen(['xdg-open', self.culled_dir])
else:
self.status_var.set("No culled directory exists.")
def open_settings(self):
"""
Opens a settings window for the application.
"""
pad_x = 10
pad_y = 10
settings_window = ctk.CTkToplevel(self.master)
settings_window.title("Piccull Settings")
settings_window.grab_set()
shortcuts_label = ctk.CTkLabel(settings_window, text="Shortcuts:")
shortcuts_label.grid(row=0, column=0, columnspan=2, padx=pad_x, pady=pad_y)
self.keybindings_entries.clear()
for idx, (action, key) in enumerate(self.keybindings.items()):
label = ctk.CTkLabel(settings_window, text=f"{action}:")
label.grid(row=1+idx, column=0, sticky='w', padx=pad_x, pady=pad_y)
entry = ctk.CTkEntry(settings_window)
entry.grid(row=1+idx, column=1, padx=pad_x, pady=pad_y)
entry.insert(0, key)
self.keybindings_entries[action] = entry
delete_checkbox = ctk.CTkCheckBox(settings_window, text="Delete on cull", variable=self.delete_on_cull)
delete_checkbox.grid(row=1+len(self.keybindings), column=0, columnspan=2, padx=pad_x, pady=pad_y)
apply_button = ctk.CTkButton(settings_window, text="Apply", command=self.apply_settings)
apply_button.grid(row=2+len(self.keybindings), column=0, columnspan=2, padx=pad_x, pady=pad_y)
def apply_settings(self):
"""
Applies the changes made in the settings window.
"""
for action, entry in self.keybindings_entries.items():
self.keybindings[action] = entry.get()
# Unbind all key events
for action in self.keybindings:
self.master.unbind(self.keybindings[action])
# Re-bind the key events
self.master.bind(self.keybindings["prev_image"], lambda e: self.prev_image())
self.master.bind(self.keybindings["next_image"], lambda e: self.next_image())
self.master.bind(self.keybindings["cull_image"], lambda e: self.cull_image())
def show_image(self):
"""
Displays the current image in the application.
"""
if self.index < len(self.image_paths):
img_path = self.image_paths[self.index]
try:
img = Image.open(img_path)
except IOError:
self.status_var.set(f"Unable to open image at {img_path}. It might be corrupted.")
self.update_button_states()
return
width, height = img.size
ratio = min(800 / width, 600 / height)
photo = ctk.CTkImage(img, size=(int(width * ratio), int(height * ratio)))
self.img_label.configure(image=photo, text="")
self.img_label.configure(image=photo)
self.img_label.image = photo
self.status_var.set(f"Directory: {self.directory_path}. Image {self.index+1}/{len(self.image_paths)}")
else:
self.img_label.configure(image=None, text="No image loaded.")
self.img_label.configure(image=None)
self.img_label.image = None
self.status_var.set("No more images in the directory.")
self.update_button_states()
def cull_image(self):
"""
Culls the current image. The image is either deleted or moved to a
'culled' folder.
"""
if self.index < len(self.image_paths):
if not os.path.exists(self.culled_dir):
os.makedirs(self.culled_dir)
if self.delete_on_cull.get():
os.remove(self.image_paths[self.index])
else:
shutil.move(self.image_paths[self.index], self.culled_dir)
del self.image_paths[self.index]
self.update_button_states()
self.btn_open_culled.configure(state='normal')
self.show_image()
def prev_image(self):
"""
Displays the previous image in the application.
"""
if self.index > 0:
self.index -= 1
self.update_button_states()
self.show_image()
def next_image(self):
"""
Displays the next image in the application.
"""
if self.index < len(self.image_paths) - 1:
self.index += 1
self.update_button_states()
self.show_image()
def update_button_states(self):
"""
Updates the state of the navigation and cull buttons based on the
current image index.
"""
if self.index <= 0:
self.btn_prev.configure(state='disabled')
else:
self.btn_prev.configure(state='normal')
if self.index >= len(self.image_paths) - 1:
self.btn_next.configure(state='disabled')
else:
self.btn_next.configure(state='normal')
if len(self.image_paths) == 0:
self.btn_cull.configure(state='disabled')
else:
self.btn_cull.configure(state='normal')
# Initialize the application
root = ctk.CTk()
app = PicCull(root)
# Start the application's main loop
root.mainloop()