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.
Files changed (40) hide show
  1. Photo_Composition_Designer/__init__.py +0 -0
  2. Photo_Composition_Designer/__main__.py +8 -0
  3. Photo_Composition_Designer/_version.py +34 -0
  4. Photo_Composition_Designer/cli/__init__.py +0 -0
  5. Photo_Composition_Designer/cli/__main__.py +8 -0
  6. Photo_Composition_Designer/cli/cli.py +106 -0
  7. Photo_Composition_Designer/common/Anniversaries.py +93 -0
  8. Photo_Composition_Designer/common/Locations.py +87 -0
  9. Photo_Composition_Designer/common/MoonPhase.py +85 -0
  10. Photo_Composition_Designer/common/Photo.py +113 -0
  11. Photo_Composition_Designer/common/__init__.py +0 -0
  12. Photo_Composition_Designer/common/logging.py +216 -0
  13. Photo_Composition_Designer/config/__init__.py +0 -0
  14. Photo_Composition_Designer/config/config.py +321 -0
  15. Photo_Composition_Designer/core/__init__.py +0 -0
  16. Photo_Composition_Designer/core/base.py +383 -0
  17. Photo_Composition_Designer/gui/GuiLogWriter.py +79 -0
  18. Photo_Composition_Designer/gui/__init__.py +0 -0
  19. Photo_Composition_Designer/gui/__main__.py +8 -0
  20. Photo_Composition_Designer/gui/gui.py +565 -0
  21. Photo_Composition_Designer/image/CalendarRenderer.py +319 -0
  22. Photo_Composition_Designer/image/CollageRenderer.py +433 -0
  23. Photo_Composition_Designer/image/DescriptionRenderer.py +74 -0
  24. Photo_Composition_Designer/image/MapRenderer.py +101 -0
  25. Photo_Composition_Designer/image/__init__.py +0 -0
  26. Photo_Composition_Designer/tools/DescriptionsFileGenerator.py +44 -0
  27. Photo_Composition_Designer/tools/GeoPlotter.py +211 -0
  28. Photo_Composition_Designer/tools/Helpers.py +18 -0
  29. Photo_Composition_Designer/tools/ImageDistributor.py +153 -0
  30. Photo_Composition_Designer/tools/__init__.py +0 -0
  31. __init__.py +0 -0
  32. firewall_handler.py +198 -0
  33. main.py +146 -0
  34. path_handler.py +10 -0
  35. photo_composition_designer-0.0.7.dist-info/METADATA +205 -0
  36. photo_composition_designer-0.0.7.dist-info/RECORD +40 -0
  37. photo_composition_designer-0.0.7.dist-info/WHEEL +5 -0
  38. photo_composition_designer-0.0.7.dist-info/entry_points.txt +3 -0
  39. photo_composition_designer-0.0.7.dist-info/licenses/LICENSE +24 -0
  40. 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()