Photo-Composition-Designer 0.0.7__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.
- Photo_Composition_Designer/__init__.py +0 -0
- Photo_Composition_Designer/__main__.py +8 -0
- Photo_Composition_Designer/_version.py +34 -0
- Photo_Composition_Designer/cli/__init__.py +0 -0
- Photo_Composition_Designer/cli/__main__.py +8 -0
- Photo_Composition_Designer/cli/cli.py +106 -0
- Photo_Composition_Designer/common/Anniversaries.py +93 -0
- Photo_Composition_Designer/common/Locations.py +87 -0
- Photo_Composition_Designer/common/MoonPhase.py +85 -0
- Photo_Composition_Designer/common/Photo.py +113 -0
- Photo_Composition_Designer/common/__init__.py +0 -0
- Photo_Composition_Designer/common/logging.py +216 -0
- Photo_Composition_Designer/config/__init__.py +0 -0
- Photo_Composition_Designer/config/config.py +321 -0
- Photo_Composition_Designer/core/__init__.py +0 -0
- Photo_Composition_Designer/core/base.py +383 -0
- Photo_Composition_Designer/gui/GuiLogWriter.py +79 -0
- Photo_Composition_Designer/gui/__init__.py +0 -0
- Photo_Composition_Designer/gui/__main__.py +8 -0
- Photo_Composition_Designer/gui/gui.py +565 -0
- Photo_Composition_Designer/image/CalendarRenderer.py +319 -0
- Photo_Composition_Designer/image/CollageRenderer.py +433 -0
- Photo_Composition_Designer/image/DescriptionRenderer.py +74 -0
- Photo_Composition_Designer/image/MapRenderer.py +101 -0
- Photo_Composition_Designer/image/__init__.py +0 -0
- Photo_Composition_Designer/tools/DescriptionsFileGenerator.py +44 -0
- Photo_Composition_Designer/tools/GeoPlotter.py +211 -0
- Photo_Composition_Designer/tools/Helpers.py +18 -0
- Photo_Composition_Designer/tools/ImageDistributor.py +153 -0
- Photo_Composition_Designer/tools/__init__.py +0 -0
- __init__.py +0 -0
- firewall_handler.py +198 -0
- main.py +146 -0
- path_handler.py +10 -0
- photo_composition_designer-0.0.7.dist-info/METADATA +205 -0
- photo_composition_designer-0.0.7.dist-info/RECORD +40 -0
- photo_composition_designer-0.0.7.dist-info/WHEEL +5 -0
- photo_composition_designer-0.0.7.dist-info/entry_points.txt +3 -0
- photo_composition_designer-0.0.7.dist-info/licenses/LICENSE +24 -0
- photo_composition_designer-0.0.7.dist-info/top_level.txt +5 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""GUI interface for Photo-Composition-Designer using tkinter with integrated logging.
|
|
2
|
+
|
|
3
|
+
This module provides a graphical user interface for the Photo-Composition-Designer
|
|
4
|
+
with settings dialog, file management, and centralized logging capabilities.
|
|
5
|
+
|
|
6
|
+
run gui: python -m Photo_Composition_Designer.gui
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import tkinter as tk
|
|
15
|
+
import traceback
|
|
16
|
+
import webbrowser
|
|
17
|
+
from datetime import timedelta
|
|
18
|
+
from functools import partial
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from tkinter import filedialog, messagebox, ttk
|
|
21
|
+
|
|
22
|
+
from config_cli_gui.gui import SettingsDialogGenerator, ToolTip
|
|
23
|
+
from PIL import Image, ImageTk
|
|
24
|
+
|
|
25
|
+
from Photo_Composition_Designer.common.logging import (
|
|
26
|
+
connect_gui_logging,
|
|
27
|
+
disconnect_gui_logging,
|
|
28
|
+
get_logger,
|
|
29
|
+
initialize_logging,
|
|
30
|
+
)
|
|
31
|
+
from Photo_Composition_Designer.common.Photo import Photo, get_photos_from_dir
|
|
32
|
+
from Photo_Composition_Designer.config.config import ConfigParameterManager
|
|
33
|
+
from Photo_Composition_Designer.core.base import CompositionDesigner
|
|
34
|
+
from Photo_Composition_Designer.gui.GuiLogWriter import GuiLogWriter
|
|
35
|
+
from Photo_Composition_Designer.tools.DescriptionsFileGenerator import DescriptionsFileGenerator
|
|
36
|
+
from Photo_Composition_Designer.tools.ImageDistributor import ImageDistributor
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MainGui:
|
|
40
|
+
"""Main GUI application class."""
|
|
41
|
+
|
|
42
|
+
distribution_modes = [
|
|
43
|
+
("distribute_equally", "Distribute photos equally"),
|
|
44
|
+
("distribute_randomly", "Distribute photos randomly"),
|
|
45
|
+
("distribute_group_matching_dates", "Distribute photos by date"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
def __init__(self, root):
|
|
49
|
+
self.root = root
|
|
50
|
+
self.root.title("Photo-Composition-Designer")
|
|
51
|
+
self.root.geometry("1200x800") # Increased width for new layout
|
|
52
|
+
self.root.update_idletasks()
|
|
53
|
+
|
|
54
|
+
# Initialize configuration
|
|
55
|
+
self.config_manager = ConfigParameterManager()
|
|
56
|
+
|
|
57
|
+
self._build_widgets()
|
|
58
|
+
self._create_menu()
|
|
59
|
+
self._reload_config()
|
|
60
|
+
|
|
61
|
+
# Setup GUI logging after widgets are created
|
|
62
|
+
self._setup_gui_logging()
|
|
63
|
+
|
|
64
|
+
# Handle window closing
|
|
65
|
+
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
|
66
|
+
self.logger.info("GUI application started")
|
|
67
|
+
|
|
68
|
+
def _reload_config(self):
|
|
69
|
+
# Initialize logging system
|
|
70
|
+
self.logger_manager = initialize_logging(self.config_manager)
|
|
71
|
+
self.logger = get_logger("gui.main")
|
|
72
|
+
|
|
73
|
+
# File lists
|
|
74
|
+
self.composition_designer = CompositionDesigner(self.config_manager)
|
|
75
|
+
self.preview_image_original = None
|
|
76
|
+
|
|
77
|
+
self.photo_folders = []
|
|
78
|
+
self.generated_compositions = []
|
|
79
|
+
|
|
80
|
+
# Load initial folder list
|
|
81
|
+
self._load_photo_folders()
|
|
82
|
+
|
|
83
|
+
self.logger.info(f"Photo directory: {self.composition_designer.photoDir}")
|
|
84
|
+
self.logger_manager.log_config_summary()
|
|
85
|
+
self._generate_preview(1)
|
|
86
|
+
self._generate_preview(0)
|
|
87
|
+
|
|
88
|
+
def _build_widgets(self):
|
|
89
|
+
"""Build the main GUI widgets using paned windows for full resize behavior."""
|
|
90
|
+
# Main container
|
|
91
|
+
main_frame = ttk.Frame(self.root)
|
|
92
|
+
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
93
|
+
|
|
94
|
+
# === TOP-LEVEL PANED WINDOW (vertical: top area + log area) ===
|
|
95
|
+
vertical_paned = ttk.PanedWindow(main_frame, orient=tk.VERTICAL)
|
|
96
|
+
vertical_paned.pack(fill=tk.BOTH, expand=True)
|
|
97
|
+
|
|
98
|
+
# === UPPER PANED (horizontal: file list + preview + fixed button panel) ===
|
|
99
|
+
top_paned = ttk.PanedWindow(vertical_paned, orient=tk.HORIZONTAL)
|
|
100
|
+
vertical_paned.add(top_paned, weight=4)
|
|
101
|
+
|
|
102
|
+
# -------------------------------------------------------------
|
|
103
|
+
# LEFT SIDE — Photo Folder List
|
|
104
|
+
# -------------------------------------------------------------
|
|
105
|
+
photo_dir_frame = ttk.LabelFrame(top_paned, text="Photo Folders")
|
|
106
|
+
top_paned.add(photo_dir_frame, weight=1)
|
|
107
|
+
|
|
108
|
+
self.photo_dir_listbox = tk.Listbox(photo_dir_frame, selectmode=tk.EXTENDED)
|
|
109
|
+
input_file_scrollbar = ttk.Scrollbar(
|
|
110
|
+
photo_dir_frame, orient="vertical", command=self.photo_dir_listbox.yview
|
|
111
|
+
)
|
|
112
|
+
self.photo_dir_listbox.configure(yscrollcommand=input_file_scrollbar.set)
|
|
113
|
+
|
|
114
|
+
self.photo_dir_listbox.pack(side="left", fill="both", expand=True, padx=5, pady=5)
|
|
115
|
+
input_file_scrollbar.pack(side="right", fill="y", pady=5)
|
|
116
|
+
|
|
117
|
+
self.photo_dir_listbox.bind(
|
|
118
|
+
"<Double-Button-1>", lambda event: self._open_selected_file(event, self.photo_folders)
|
|
119
|
+
)
|
|
120
|
+
self.photo_dir_listbox.bind(
|
|
121
|
+
"<Button-1>", lambda event: self._generate_preview_callback(event)
|
|
122
|
+
)
|
|
123
|
+
self.photo_dir_listbox.bind("<Up>", lambda event: self._generate_preview_callback())
|
|
124
|
+
self.photo_dir_listbox.bind("<Down>", lambda event: self._generate_preview_callback())
|
|
125
|
+
|
|
126
|
+
# -------------------------------------------------------------
|
|
127
|
+
# CENTER — Preview panel
|
|
128
|
+
# -------------------------------------------------------------
|
|
129
|
+
self.image_frame = ttk.LabelFrame(top_paned, text="Preview")
|
|
130
|
+
top_paned.add(self.image_frame, weight=6)
|
|
131
|
+
|
|
132
|
+
self.preview_label = ttk.Label(self.image_frame)
|
|
133
|
+
self.preview_label.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
134
|
+
self.preview_label.bind("<Configure>", self._refresh_preview)
|
|
135
|
+
self.image_frame.bind("<Configure>", self._refresh_preview)
|
|
136
|
+
self.preview_label.bind("<Configure>", self._refresh_preview)
|
|
137
|
+
|
|
138
|
+
# -------------------------------------------------------------
|
|
139
|
+
# RIGHT SIDE — FIXED-WIDTH BUTTON PANEL
|
|
140
|
+
# -------------------------------------------------------------
|
|
141
|
+
button_outer_frame = ttk.Frame(top_paned)
|
|
142
|
+
# Add with weight=0 to keep fixed width
|
|
143
|
+
top_paned.add(button_outer_frame, weight=0)
|
|
144
|
+
|
|
145
|
+
# inner frame for padding
|
|
146
|
+
button_frame = ttk.Frame(button_outer_frame)
|
|
147
|
+
button_frame.pack(fill=tk.Y, padx=5, pady=5)
|
|
148
|
+
|
|
149
|
+
# folder selection
|
|
150
|
+
select_config_button = ttk.Button(
|
|
151
|
+
button_frame, text="Select config file", command=self._select_config
|
|
152
|
+
)
|
|
153
|
+
select_config_button.pack(pady=10, fill=tk.X)
|
|
154
|
+
|
|
155
|
+
# dynamic run buttons
|
|
156
|
+
self.run_buttons = {}
|
|
157
|
+
for mode, label in self.distribution_modes:
|
|
158
|
+
button = ttk.Button(
|
|
159
|
+
button_frame, text=label, command=partial(self._run_processing, mode=mode)
|
|
160
|
+
)
|
|
161
|
+
ToolTip(button, f"Distribute all images into folders using \nthe method '{label}'")
|
|
162
|
+
button.pack(pady=2, fill=tk.X)
|
|
163
|
+
self.run_buttons[mode] = button
|
|
164
|
+
|
|
165
|
+
# description file button
|
|
166
|
+
self.generate_description_file_button = ttk.Button(
|
|
167
|
+
button_frame,
|
|
168
|
+
text="Generate Description File",
|
|
169
|
+
command=self._generate_template_description_file,
|
|
170
|
+
)
|
|
171
|
+
ToolTip(
|
|
172
|
+
self.generate_description_file_button,
|
|
173
|
+
"Generate a template description file for all collages \n"
|
|
174
|
+
"based on the generated photo directories",
|
|
175
|
+
)
|
|
176
|
+
self.generate_description_file_button.pack(pady=20, fill=tk.X)
|
|
177
|
+
|
|
178
|
+
# compositions button
|
|
179
|
+
self.generate_compositions_button = ttk.Button(
|
|
180
|
+
button_frame, text="Generate Compositions", command=self._generate_compositions
|
|
181
|
+
)
|
|
182
|
+
self.generate_compositions_button.pack(pady=0, fill=tk.X)
|
|
183
|
+
|
|
184
|
+
# progress bar
|
|
185
|
+
self.progress = ttk.Progressbar(button_frame, mode="indeterminate")
|
|
186
|
+
self.progress.pack(
|
|
187
|
+
pady=10,
|
|
188
|
+
fill=tk.X,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# === LOWER AREA — Log Output ===
|
|
192
|
+
log_frame = ttk.LabelFrame(vertical_paned, text="Log Output")
|
|
193
|
+
vertical_paned.add(log_frame, weight=1)
|
|
194
|
+
|
|
195
|
+
# log text area with scrollbar
|
|
196
|
+
log_text_frame = ttk.Frame(log_frame)
|
|
197
|
+
log_text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
198
|
+
|
|
199
|
+
self.log_text = tk.Text(log_text_frame, height=10, wrap=tk.WORD)
|
|
200
|
+
log_text_scrollbar = ttk.Scrollbar(
|
|
201
|
+
log_text_frame, orient="vertical", command=self.log_text.yview
|
|
202
|
+
)
|
|
203
|
+
self.log_text.configure(yscrollcommand=log_text_scrollbar.set)
|
|
204
|
+
|
|
205
|
+
self.log_text.pack(side="left", fill="both", expand=True)
|
|
206
|
+
log_text_scrollbar.pack(side="right", fill="y")
|
|
207
|
+
|
|
208
|
+
# log controls
|
|
209
|
+
log_controls = ttk.Frame(log_frame)
|
|
210
|
+
log_controls.pack(fill=tk.X, padx=5, pady=(0, 5))
|
|
211
|
+
|
|
212
|
+
ttk.Button(log_controls, text="Clear Log", command=self._clear_log).pack(side=tk.LEFT)
|
|
213
|
+
|
|
214
|
+
ttk.Label(log_controls, text="Log Level:").pack(side=tk.LEFT, padx=(10, 5))
|
|
215
|
+
self.log_level_var = tk.StringVar(value=self.config_manager.app.log_level.value)
|
|
216
|
+
|
|
217
|
+
log_level_combo = ttk.Combobox(
|
|
218
|
+
log_controls,
|
|
219
|
+
textvariable=self.log_level_var,
|
|
220
|
+
values=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
221
|
+
state="readonly",
|
|
222
|
+
width=10,
|
|
223
|
+
)
|
|
224
|
+
log_level_combo.pack(side=tk.LEFT)
|
|
225
|
+
log_level_combo.bind("<<ComboboxSelected>>", self._on_log_level_changed)
|
|
226
|
+
|
|
227
|
+
def _create_menu(self):
|
|
228
|
+
"""Create the application menu."""
|
|
229
|
+
menubar = tk.Menu(self.root)
|
|
230
|
+
self.root.config(menu=menubar)
|
|
231
|
+
|
|
232
|
+
# File menu
|
|
233
|
+
file_menu = tk.Menu(menubar, tearoff=0)
|
|
234
|
+
menubar.add_cascade(label="File", menu=file_menu)
|
|
235
|
+
file_menu.add_command(label="Open...", command=self._select_config)
|
|
236
|
+
file_menu.add_separator()
|
|
237
|
+
|
|
238
|
+
# Create Run menu options dynamically
|
|
239
|
+
for mode, label in self.distribution_modes:
|
|
240
|
+
file_menu.add_command(label=label, command=partial(self._run_processing, mode=mode))
|
|
241
|
+
|
|
242
|
+
file_menu.add_separator()
|
|
243
|
+
file_menu.add_command(label="Exit", command=self._on_closing)
|
|
244
|
+
|
|
245
|
+
# Options menu
|
|
246
|
+
options_menu = tk.Menu(menubar, tearoff=0)
|
|
247
|
+
menubar.add_cascade(label="Options", menu=options_menu)
|
|
248
|
+
options_menu.add_command(label="Settings", command=self._open_settings)
|
|
249
|
+
|
|
250
|
+
# Help menu
|
|
251
|
+
help_menu = tk.Menu(menubar, tearoff=0)
|
|
252
|
+
menubar.add_cascade(label="Help", menu=help_menu)
|
|
253
|
+
help_menu.add_command(label="User help", command=self._open_help)
|
|
254
|
+
help_menu.add_separator()
|
|
255
|
+
help_menu.add_command(label="About", command=self._show_about)
|
|
256
|
+
|
|
257
|
+
def _generate_preview_callback(self, event=None):
|
|
258
|
+
if event:
|
|
259
|
+
selection_index = event.widget.nearest(event.y)
|
|
260
|
+
if selection_index == -1:
|
|
261
|
+
return
|
|
262
|
+
else:
|
|
263
|
+
selection = self.photo_dir_listbox.curselection()
|
|
264
|
+
if not selection:
|
|
265
|
+
return
|
|
266
|
+
selection_index = selection[0]
|
|
267
|
+
self._generate_preview(selection_index)
|
|
268
|
+
|
|
269
|
+
def _generate_preview(self, selection_index):
|
|
270
|
+
folder_name = self.photo_folders[selection_index].name
|
|
271
|
+
preview_image: Image.Image = self.composition_designer.generate_compositions_from_folder(
|
|
272
|
+
folder_name
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if not preview_image:
|
|
276
|
+
self.logger.info(f"Empty folder '{folder_name}'. No preview available.")
|
|
277
|
+
return
|
|
278
|
+
# Save original unscaled image
|
|
279
|
+
img = preview_image.copy()
|
|
280
|
+
self.preview_image_original = img
|
|
281
|
+
|
|
282
|
+
# Scale to current preview widget
|
|
283
|
+
w = self.preview_label.winfo_width()
|
|
284
|
+
h = self.preview_label.winfo_height()
|
|
285
|
+
|
|
286
|
+
if w > 0 and h > 0:
|
|
287
|
+
img.thumbnail((w, h))
|
|
288
|
+
|
|
289
|
+
self.preview_photo = ImageTk.PhotoImage(img)
|
|
290
|
+
self.preview_label.configure(image=self.preview_photo)
|
|
291
|
+
|
|
292
|
+
self.logger.info(f"Preview generated for folder {folder_name}")
|
|
293
|
+
|
|
294
|
+
# self.preview_label.after(10, lambda: self._refresh_preview(tk.Event()))
|
|
295
|
+
|
|
296
|
+
def _load_photo_folders(self):
|
|
297
|
+
"""Scan self.photo_dir for subfolders and populate the listbox and internal list."""
|
|
298
|
+
|
|
299
|
+
# Clear previous content
|
|
300
|
+
self.photo_dir_listbox.delete(0, tk.END)
|
|
301
|
+
self.photo_folders = []
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
not self.composition_designer.photoDir.exists()
|
|
305
|
+
or not self.composition_designer.photoDir.is_dir()
|
|
306
|
+
):
|
|
307
|
+
self.logger.warning(
|
|
308
|
+
f"Photo directory '{self.composition_designer.photoDir}' does not exist."
|
|
309
|
+
)
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Collect subfolder names, sorted alphabetically
|
|
313
|
+
subfolders = sorted(
|
|
314
|
+
[item for item in self.composition_designer.photoDir.iterdir() if item.is_dir()],
|
|
315
|
+
key=lambda p: p.name.lower(),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Populate internal list AND the listbox
|
|
319
|
+
for folder in subfolders:
|
|
320
|
+
self.photo_folders.append(folder)
|
|
321
|
+
self.photo_dir_listbox.insert(tk.END, folder.name)
|
|
322
|
+
|
|
323
|
+
self.logger.info(f"Loaded {len(self.photo_folders)} photo subfolders.")
|
|
324
|
+
|
|
325
|
+
def _setup_gui_logging(self):
|
|
326
|
+
"""Setup GUI logging integration."""
|
|
327
|
+
# Create GUI log writer
|
|
328
|
+
self.gui_log_writer = GuiLogWriter(self.log_text)
|
|
329
|
+
|
|
330
|
+
# Connect to logging system
|
|
331
|
+
connect_gui_logging(self.gui_log_writer)
|
|
332
|
+
|
|
333
|
+
def _on_log_level_changed(self, event=None):
|
|
334
|
+
"""Handle log level change."""
|
|
335
|
+
new_level = self.log_level_var.get()
|
|
336
|
+
self.logger_manager.set_log_level(new_level)
|
|
337
|
+
self.logger.info(f"Log level changed to {new_level}")
|
|
338
|
+
|
|
339
|
+
def _clear_log(self):
|
|
340
|
+
"""Clear the log text widget."""
|
|
341
|
+
self.log_text.delete(1.0, tk.END)
|
|
342
|
+
self.logger.debug("Log display cleared")
|
|
343
|
+
|
|
344
|
+
def _generate_compositions(self):
|
|
345
|
+
"""Generate all compositions"""
|
|
346
|
+
pass
|
|
347
|
+
self.logger.info("Generate all compositions...")
|
|
348
|
+
self.composition_designer.generate_compositions_from_folders()
|
|
349
|
+
self.logger.info("Compositions generated")
|
|
350
|
+
|
|
351
|
+
def _open_selected_file(self, event, file_list_source):
|
|
352
|
+
"""Opens the selected file in the system's default application or explorer."""
|
|
353
|
+
selection_index = event.widget.nearest(event.y)
|
|
354
|
+
if selection_index == -1: # No item clicked
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
file_path_str = file_list_source[selection_index]["path"]
|
|
358
|
+
file_path = Path(file_path_str)
|
|
359
|
+
|
|
360
|
+
if not file_path.exists():
|
|
361
|
+
self.logger.error(f"File not found: {file_path}")
|
|
362
|
+
messagebox.showerror("Error", f"File not found: {file_path}")
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
if sys.platform == "win32":
|
|
367
|
+
os.startfile(file_path)
|
|
368
|
+
elif sys.platform == "darwin":
|
|
369
|
+
subprocess.Popen(["open", file_path])
|
|
370
|
+
else:
|
|
371
|
+
subprocess.Popen(["xdg-open", file_path])
|
|
372
|
+
self.logger.info(f"Opened file: {file_path}")
|
|
373
|
+
except Exception as e:
|
|
374
|
+
self.logger.error(f"Could not open file {file_path}: {e}")
|
|
375
|
+
messagebox.showerror("Error", f"Could not open file {file_path}: {e}")
|
|
376
|
+
|
|
377
|
+
def _select_config(self):
|
|
378
|
+
"""Open file dialog open config file."""
|
|
379
|
+
config_file = filedialog.askopenfilename(
|
|
380
|
+
title="Select config file",
|
|
381
|
+
filetypes=[("YAML files", "*.yaml")],
|
|
382
|
+
)
|
|
383
|
+
if config_file:
|
|
384
|
+
self.logger.info(f"Load {config_file} as new base config")
|
|
385
|
+
self.config_manager = ConfigParameterManager(config_file)
|
|
386
|
+
self._reload_config()
|
|
387
|
+
else:
|
|
388
|
+
self.logger.debug("No valid config file selected")
|
|
389
|
+
self._generate_preview(0)
|
|
390
|
+
|
|
391
|
+
def _run_processing(self, mode="distribute_group_matching_dates"):
|
|
392
|
+
"""Run the processing in a separate thread."""
|
|
393
|
+
|
|
394
|
+
self.logger.info(f"Starting distribution of in mode: {mode}")
|
|
395
|
+
|
|
396
|
+
# Disable all buttons during processing
|
|
397
|
+
for button in self.run_buttons.values():
|
|
398
|
+
button.config(state="disabled")
|
|
399
|
+
|
|
400
|
+
self.generate_compositions_button.config(state="disabled")
|
|
401
|
+
self.progress.start()
|
|
402
|
+
|
|
403
|
+
# Run in separate thread to avoid blocking GUI
|
|
404
|
+
thread = threading.Thread(
|
|
405
|
+
target=self._distribute_images,
|
|
406
|
+
args=(mode,),
|
|
407
|
+
daemon=True,
|
|
408
|
+
)
|
|
409
|
+
thread.start()
|
|
410
|
+
|
|
411
|
+
def _distribute_images(self, mode="compress_files"):
|
|
412
|
+
"""Process the selected files."""
|
|
413
|
+
grouped_images = []
|
|
414
|
+
try:
|
|
415
|
+
self.logger.info("=== Processing Started ===")
|
|
416
|
+
self.logger.info("Processing files...")
|
|
417
|
+
|
|
418
|
+
# prepare image sorting:
|
|
419
|
+
photos: list[Photo] = get_photos_from_dir(self.composition_designer.photoDir)
|
|
420
|
+
|
|
421
|
+
# prepare image distribution
|
|
422
|
+
collages_to_generate = self.config_manager.calendar.collagesToGenerate.value
|
|
423
|
+
image_distributor = ImageDistributor(photos, collages_to_generate)
|
|
424
|
+
# implement switch case for different processing modes
|
|
425
|
+
if mode == "distribute_equally":
|
|
426
|
+
grouped_images = image_distributor.distribute_equally()
|
|
427
|
+
elif mode == "distribute_randomly":
|
|
428
|
+
grouped_images = image_distributor.distribute_randomly()
|
|
429
|
+
elif mode == "distribute_group_matching_dates":
|
|
430
|
+
grouped_images = image_distributor.distribute_group_matching_dates()
|
|
431
|
+
else:
|
|
432
|
+
self.logger.warning(f"Unknown mode: {mode}")
|
|
433
|
+
|
|
434
|
+
start_date = self.config_manager.calendar.startDate.value
|
|
435
|
+
output_dir = self.composition_designer.photoDir
|
|
436
|
+
for week in range(collages_to_generate):
|
|
437
|
+
week_start = start_date + timedelta(weeks=week)
|
|
438
|
+
folder_name = f"{week:02d}_{week_start.strftime('%b-%d')}"
|
|
439
|
+
folder_path = os.path.join(output_dir, folder_name)
|
|
440
|
+
os.makedirs(folder_path, exist_ok=True)
|
|
441
|
+
self.logger.info(f"Folder created: {folder_path}")
|
|
442
|
+
|
|
443
|
+
if not grouped_images:
|
|
444
|
+
continue
|
|
445
|
+
images_in_group = grouped_images.pop(0)
|
|
446
|
+
for photo in images_in_group:
|
|
447
|
+
image_file_name = photo.file_path.name
|
|
448
|
+
destination_path = os.path.join(folder_path, image_file_name)
|
|
449
|
+
shutil.copy2(photo.file_path, destination_path)
|
|
450
|
+
self.logger.info(
|
|
451
|
+
f" --> Image {photo.file_path.name} sorted into {folder_name}"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
self.logger.info(f"Completed: {len(grouped_images)} files processed")
|
|
455
|
+
self.logger.info("=== All files processed successfully! ===")
|
|
456
|
+
|
|
457
|
+
except Exception as err:
|
|
458
|
+
self.logger.error(f"Processing failed: {err}", exc_info=True)
|
|
459
|
+
# Show error dialog in main thread
|
|
460
|
+
self.root.after(
|
|
461
|
+
0, lambda e=err: messagebox.showerror("Error", f"Processing failed: {e}")
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
finally:
|
|
465
|
+
# Re-enable controls in main thread
|
|
466
|
+
self.root.after(0, self._processing_finished)
|
|
467
|
+
|
|
468
|
+
def _processing_finished(self):
|
|
469
|
+
"""Re-enable controls after processing is finished."""
|
|
470
|
+
for button in self.run_buttons.values():
|
|
471
|
+
button.config(state="normal")
|
|
472
|
+
|
|
473
|
+
self.generate_compositions_button.config(state="normal")
|
|
474
|
+
self.progress.stop()
|
|
475
|
+
|
|
476
|
+
def _open_settings(self):
|
|
477
|
+
"""Open the settings dialog."""
|
|
478
|
+
self.logger.debug("Opening settings dialog")
|
|
479
|
+
settings_dialog_generator = SettingsDialogGenerator(self.config_manager)
|
|
480
|
+
dialog = settings_dialog_generator.create_settings_dialog(self.root)
|
|
481
|
+
self.root.wait_window(dialog.dialog)
|
|
482
|
+
|
|
483
|
+
if dialog.result == "ok":
|
|
484
|
+
self.logger.info("Settings updated successfully")
|
|
485
|
+
# Update log level selector if it changed
|
|
486
|
+
self.log_level_var.set(self.config_manager.app.log_level.value)
|
|
487
|
+
self._reload_config()
|
|
488
|
+
|
|
489
|
+
def _open_help(self):
|
|
490
|
+
"""Open help documentation in browser."""
|
|
491
|
+
self.logger.debug("Opening help documentation")
|
|
492
|
+
webbrowser.open("https://Photo-Composition-Designer.readthedocs.io/en/stable/")
|
|
493
|
+
|
|
494
|
+
def _show_about(self):
|
|
495
|
+
"""Show about dialog."""
|
|
496
|
+
self.logger.debug("Showing about dialog")
|
|
497
|
+
messagebox.showinfo("About", "Photo-Composition-Designer\n\nCopyright by Paul")
|
|
498
|
+
|
|
499
|
+
def _on_closing(self):
|
|
500
|
+
"""Handle application closing."""
|
|
501
|
+
self.logger.info("Closing GUI application")
|
|
502
|
+
disconnect_gui_logging()
|
|
503
|
+
self.root.quit()
|
|
504
|
+
self.root.destroy()
|
|
505
|
+
|
|
506
|
+
def _generate_template_description_file(self):
|
|
507
|
+
description_file_gen = DescriptionsFileGenerator(
|
|
508
|
+
self.composition_designer.photoDir,
|
|
509
|
+
self.composition_designer.outputDir,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
if description_file_gen.description_file_exists():
|
|
513
|
+
# ask user for overwrite permission
|
|
514
|
+
overwrite = messagebox.askyesno(
|
|
515
|
+
"Overwrite?", "A description file already exists. Do you want to overwrite it?"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if not overwrite:
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
description_file = description_file_gen.generate_description_file(overwrite=True)
|
|
522
|
+
self.logger.info(f"Template description file generated: {description_file}")
|
|
523
|
+
|
|
524
|
+
def _refresh_preview(self, event=None):
|
|
525
|
+
if not hasattr(self, "preview_image_original"):
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
if not self.preview_image_original:
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
height = (
|
|
532
|
+
event.height
|
|
533
|
+
if event is not None and hasattr(event, "height")
|
|
534
|
+
else self.preview_label.winfo_height()
|
|
535
|
+
)
|
|
536
|
+
width = (
|
|
537
|
+
event.width
|
|
538
|
+
if event is not None and hasattr(event, "width")
|
|
539
|
+
else self.preview_label.winfo_width()
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
margin = 30 # extra padding around the preview
|
|
543
|
+
if width <= margin or height <= margin:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
img = self.preview_image_original.copy()
|
|
547
|
+
img.thumbnail((width - margin, height - margin))
|
|
548
|
+
self.preview_photo = ImageTk.PhotoImage(img)
|
|
549
|
+
self.preview_label.configure(image=self.preview_photo, anchor="center", compound="")
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def main():
|
|
553
|
+
"""Main entry point for the GUI application."""
|
|
554
|
+
root = tk.Tk()
|
|
555
|
+
try:
|
|
556
|
+
MainGui(root)
|
|
557
|
+
root.mainloop()
|
|
558
|
+
except Exception as e:
|
|
559
|
+
print(f"GUI startup failed: {e}")
|
|
560
|
+
traceback.print_exc()
|
|
561
|
+
sys.exit(1)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
if __name__ == "__main__":
|
|
565
|
+
main()
|