python-template-project 0.1.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.
@@ -0,0 +1,615 @@
1
+ """GUI interface for python_template_project using tkinter with integrated logging.
2
+
3
+ This module provides a graphical user interface for the python_template_project
4
+ with settings dialog, file management, and centralized logging capabilities.
5
+
6
+ run gui: python -m python_template_project.gui
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import threading
12
+ import tkinter as tk
13
+ import traceback
14
+ import webbrowser
15
+ from functools import partial
16
+ from tkinter import filedialog, messagebox, ttk
17
+
18
+ from python_template_project.config.config import ConfigParameter, ConfigParameterManager
19
+ from python_template_project.core.base import BaseGPXProcessor
20
+ from python_template_project.core.logging import (
21
+ connect_gui_logging,
22
+ disconnect_gui_logging,
23
+ get_logger,
24
+ initialize_logging,
25
+ )
26
+
27
+
28
+ class ToolTip:
29
+ """Create a tooltip for a given widget."""
30
+
31
+ def __init__(self, widget, text="widget info"):
32
+ self.widget = widget
33
+ self.text = text
34
+ self.tipwindow = None
35
+ self.id = None
36
+ self.x = self.y = 0
37
+
38
+ self.widget.bind("<Enter>", self.on_enter)
39
+ self.widget.bind("<Leave>", self.on_leave)
40
+
41
+ def on_enter(self, event=None):
42
+ self.show_tooltip()
43
+
44
+ def on_leave(self, event=None):
45
+ self.hide_tooltip()
46
+
47
+ def show_tooltip(self):
48
+ if self.tipwindow or not self.text:
49
+ return
50
+ x, y, cx, cy = self.widget.bbox("insert")
51
+ x = x + self.widget.winfo_rootx() + 25
52
+ y = y + cy + self.widget.winfo_rooty() + 25
53
+ self.tipwindow = tw = tk.Toplevel(self.widget)
54
+ tw.wm_overrideredirect(True)
55
+ tw.wm_geometry(f"+{x}+{y}")
56
+ label = tk.Label(
57
+ tw,
58
+ text=self.text,
59
+ justify=tk.LEFT,
60
+ background="#ffffe0",
61
+ relief=tk.SOLID,
62
+ borderwidth=1,
63
+ font=("tahoma", "8", "normal"),
64
+ )
65
+ label.pack(ipadx=1)
66
+
67
+ def hide_tooltip(self):
68
+ tw = self.tipwindow
69
+ self.tipwindow = None
70
+ if tw:
71
+ tw.destroy()
72
+
73
+
74
+ class GuiLogWriter:
75
+ """Log writer that handles GUI text widget updates in a thread-safe way."""
76
+
77
+ def __init__(self, text_widget):
78
+ self.text_widget = text_widget
79
+ self.root = text_widget.winfo_toplevel()
80
+
81
+ def write(self, text):
82
+ """Write text to the widget in a thread-safe manner."""
83
+ # Schedule the GUI update in the main thread
84
+ self.root.after(0, self._update_text, text)
85
+
86
+ def _update_text(self, text):
87
+ """Update the text widget (must be called from main thread)."""
88
+ try:
89
+ self.text_widget.insert(tk.END, text)
90
+ self.text_widget.see(tk.END)
91
+ self.text_widget.update_idletasks()
92
+ except tk.TclError:
93
+ # Widget might be destroyed
94
+ pass
95
+
96
+ def flush(self):
97
+ """Flush method for compatibility."""
98
+ pass
99
+
100
+
101
+ class SettingsDialog:
102
+ """Settings dialog for configuration management."""
103
+
104
+ def __init__(self, parent, config_manager: ConfigParameterManager):
105
+ self.parent = parent
106
+ self.config_manager = config_manager
107
+ self.result = None
108
+ self.widgets = {}
109
+ self.logger = get_logger("gui.settings")
110
+
111
+ # Create dialog window
112
+ self.dialog = tk.Toplevel(parent)
113
+ self.dialog.title("Settings")
114
+ self.dialog.geometry("600x500")
115
+ self.dialog.transient(parent)
116
+ self.dialog.grab_set()
117
+
118
+ # Center the dialog
119
+ self.dialog.geometry(f"+{int(parent.winfo_rootx() + 50)}+{int(parent.winfo_rooty() + 50)}")
120
+
121
+ self._create_widgets()
122
+
123
+ # Handle window closing
124
+ self.dialog.protocol("WM_DELETE_WINDOW", self._on_cancel)
125
+
126
+ self.logger.debug("Settings dialog opened")
127
+
128
+ def _create_widgets(self):
129
+ """Create the settings dialog widgets."""
130
+ # Main frame
131
+ main_frame = ttk.Frame(self.dialog)
132
+ main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
133
+
134
+ # Create notebook for tabs
135
+ self.notebook = ttk.Notebook(main_frame)
136
+ self.notebook.pack(fill=tk.BOTH, expand=True)
137
+
138
+ # Create tabs for each configuration category
139
+ self._create_cli_tab()
140
+ self._create_app_tab()
141
+ self._create_gui_tab()
142
+
143
+ # Button frame
144
+ button_frame = ttk.Frame(main_frame)
145
+ button_frame.pack(fill=tk.X, pady=(10, 0))
146
+
147
+ ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=(5, 0))
148
+ ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
149
+
150
+ def _create_cli_tab(self):
151
+ """Create CLI configuration tab."""
152
+ cli_frame = ttk.Frame(self.notebook)
153
+ self.notebook.add(cli_frame, text="CLI")
154
+
155
+ # Create scrollable frame
156
+ canvas = tk.Canvas(cli_frame)
157
+ scrollbar = ttk.Scrollbar(cli_frame, orient="vertical", command=canvas.yview)
158
+ scrollable_frame = ttk.Frame(canvas)
159
+
160
+ scrollable_frame.bind(
161
+ "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
162
+ )
163
+
164
+ canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
165
+ canvas.configure(yscrollcommand=scrollbar.set)
166
+
167
+ # Add parameters
168
+ self._add_category_parameters(scrollable_frame, "cli", self.config_manager.cli)
169
+
170
+ canvas.pack(side="left", fill="both", expand=True)
171
+ scrollbar.pack(side="right", fill="y")
172
+
173
+ def _create_app_tab(self):
174
+ """Create App configuration tab."""
175
+ app_frame = ttk.Frame(self.notebook)
176
+ self.notebook.add(app_frame, text="App")
177
+
178
+ # Create scrollable frame
179
+ canvas = tk.Canvas(app_frame)
180
+ scrollbar = ttk.Scrollbar(app_frame, orient="vertical", command=canvas.yview)
181
+ scrollable_frame = ttk.Frame(canvas)
182
+
183
+ scrollable_frame.bind(
184
+ "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
185
+ )
186
+
187
+ canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
188
+ canvas.configure(yscrollcommand=scrollbar.set)
189
+
190
+ # Add parameters
191
+ self._add_category_parameters(scrollable_frame, "app", self.config_manager.app)
192
+
193
+ canvas.pack(side="left", fill="both", expand=True)
194
+ scrollbar.pack(side="right", fill="y")
195
+
196
+ def _create_gui_tab(self):
197
+ """Create GUI configuration tab."""
198
+ gui_frame = ttk.Frame(self.notebook)
199
+ self.notebook.add(gui_frame, text="GUI")
200
+
201
+ # Create scrollable frame
202
+ canvas = tk.Canvas(gui_frame)
203
+ scrollbar = ttk.Scrollbar(gui_frame, orient="vertical", command=canvas.yview)
204
+ scrollable_frame = ttk.Frame(canvas)
205
+
206
+ scrollable_frame.bind(
207
+ "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
208
+ )
209
+
210
+ canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
211
+ canvas.configure(yscrollcommand=scrollbar.set)
212
+
213
+ # Add parameters
214
+ self._add_category_parameters(scrollable_frame, "gui", self.config_manager.gui)
215
+
216
+ canvas.pack(side="left", fill="both", expand=True)
217
+ scrollbar.pack(side="right", fill="y")
218
+
219
+ def _add_category_parameters(self, parent, category_name, category_obj):
220
+ """Add parameter widgets for a specific category."""
221
+ row = 0
222
+ for field_name in category_obj.model_fields:
223
+ param = getattr(category_obj, field_name)
224
+ if param.required:
225
+ # Skip required parameters as they are not configurable in GUI
226
+ # --> required params have to be set via Open file dialog in GUI
227
+ continue
228
+
229
+ # Create label
230
+ label = ttk.Label(parent, text=f"{param.name}:")
231
+ label.grid(row=row, column=0, sticky="w", padx=5, pady=2)
232
+
233
+ # Create appropriate widget based on parameter type
234
+ widget = self._create_parameter_widget(parent, param)
235
+ widget.grid(row=row, column=1, sticky="ew", padx=5, pady=2)
236
+
237
+ # Add tooltip
238
+ ToolTip(label, param.help)
239
+ ToolTip(widget, param.help)
240
+
241
+ # Store widget reference
242
+ self.widgets[f"{category_name}__{param.name}"] = widget
243
+
244
+ row += 1
245
+
246
+ # Configure column weights
247
+ parent.columnconfigure(1, weight=1)
248
+
249
+ def _create_parameter_widget(self, parent, param: ConfigParameter):
250
+ """Create appropriate widget for parameter type."""
251
+ if param.type_ == bool:
252
+ # Checkbox for boolean values
253
+ var = tk.BooleanVar(value=param.default)
254
+ widget = ttk.Checkbutton(parent, variable=var)
255
+ widget.var = var
256
+ return widget
257
+
258
+ elif param.choices and param.type_ != bool:
259
+ # Combobox for choices
260
+ var = tk.StringVar(value=str(param.default))
261
+ widget = ttk.Combobox(parent, textvariable=var, values=param.choices, state="readonly")
262
+ widget.var = var
263
+ return widget
264
+
265
+ elif param.type_ == int:
266
+ # Spinbox for integers
267
+ var = tk.IntVar(value=param.default)
268
+ widget = ttk.Spinbox(parent, from_=-999999, to=999999, textvariable=var)
269
+ widget.var = var
270
+ return widget
271
+
272
+ else: # str or other types
273
+ # Entry for strings
274
+ var = tk.StringVar(value=str(param.default))
275
+ widget = ttk.Entry(parent, textvariable=var)
276
+ widget.var = var
277
+ return widget
278
+
279
+ def _on_ok(self):
280
+ """Handle OK button click."""
281
+ try:
282
+ # Update configuration with widget values
283
+ overrides = {}
284
+ for key, widget in self.widgets.items():
285
+ value = widget.var.get()
286
+ overrides[key] = value
287
+
288
+ self.logger.info(f"Applying configuration overrides: {len(overrides)} settings")
289
+
290
+ # Apply overrides to config manager
291
+ self.config_manager._apply_kwargs(overrides)
292
+
293
+ # Save to file
294
+ self.config_manager.save_to_file("config.yaml")
295
+ self.logger.info("Configuration saved successfully")
296
+
297
+ self.result = "ok"
298
+ self.dialog.destroy()
299
+
300
+ except Exception as e:
301
+ self.logger.error(f"Failed to save configuration: {e}")
302
+ messagebox.showerror("Error", f"Failed to save configuration: {e}")
303
+
304
+ def _on_cancel(self):
305
+ """Handle Cancel button click."""
306
+ self.logger.debug("Settings dialog cancelled")
307
+ self.result = "cancel"
308
+ self.dialog.destroy()
309
+
310
+
311
+ class MainGui:
312
+ """Main GUI application class."""
313
+
314
+ processing_modes = [
315
+ ("compress_files", "Compress"),
316
+ ("merge_files", "Merge"),
317
+ ("extract_pois", "Extract POIs"),
318
+ ]
319
+
320
+ def __init__(self, root):
321
+ self.root = root
322
+ self.root.title("python-template-project")
323
+ self.root.geometry("800x600")
324
+
325
+ # Initialize configuration
326
+ self.config_manager = ConfigParameterManager()
327
+
328
+ # Initialize logging system
329
+ self.logger_manager = initialize_logging(self.config_manager)
330
+ self.logger = get_logger("gui.main")
331
+
332
+ # File list
333
+ self.file_list = []
334
+
335
+ self._build_widgets()
336
+ self._create_menu()
337
+
338
+ # Setup GUI logging after widgets are created
339
+ self._setup_gui_logging()
340
+
341
+ # Handle window closing
342
+ self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
343
+
344
+ self.logger.info("GUI application started")
345
+ self.logger_manager.log_config_summary()
346
+
347
+ def _build_widgets(self):
348
+ """Build the main GUI widgets."""
349
+ # Main container
350
+ main_frame = ttk.Frame(self.root)
351
+ main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
352
+
353
+ # Top frame for file list and run button
354
+ top_frame = ttk.Frame(main_frame)
355
+ top_frame.pack(fill=tk.BOTH, expand=True)
356
+
357
+ # Left side - File list
358
+ file_frame = ttk.LabelFrame(top_frame, text="Input Files")
359
+ file_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
360
+
361
+ # File listbox with scrollbar
362
+ listbox_frame = ttk.Frame(file_frame)
363
+ listbox_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
364
+
365
+ self.file_listbox = tk.Listbox(listbox_frame)
366
+ file_scrollbar = ttk.Scrollbar(
367
+ listbox_frame, orient="vertical", command=self.file_listbox.yview
368
+ )
369
+ self.file_listbox.configure(yscrollcommand=file_scrollbar.set)
370
+
371
+ self.file_listbox.pack(side="left", fill="both", expand=True)
372
+ file_scrollbar.pack(side="right", fill="y")
373
+
374
+ # Right side - Run button and controls
375
+ button_frame = ttk.Frame(top_frame)
376
+ button_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(5, 0))
377
+
378
+ open_button = ttk.Button(button_frame, text="Open Files", command=self._open_files)
379
+ open_button.pack(pady=8)
380
+
381
+ # Create buttons dynamically
382
+ self.run_buttons = {}
383
+ for mode, label in self.processing_modes:
384
+ button = ttk.Button(
385
+ button_frame, text=label, command=partial(self._run_processing, mode=mode)
386
+ )
387
+ button.pack(pady=1)
388
+ # Save buttons in dictionary for later access
389
+ self.run_buttons[mode] = button
390
+
391
+ # Clear files button
392
+ self.clear_button = ttk.Button(button_frame, text="Clear Files", command=self._clear_files)
393
+ self.clear_button.pack(pady=8)
394
+
395
+ # Progress bar
396
+ self.progress = ttk.Progressbar(button_frame, mode="indeterminate")
397
+ self.progress.pack(pady=5, fill=tk.X)
398
+
399
+ # Bottom frame - Log output
400
+ log_frame = ttk.LabelFrame(main_frame, text="Log Output")
401
+ log_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
402
+
403
+ # Log text widget with scrollbar
404
+ log_text_frame = ttk.Frame(log_frame)
405
+ log_text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
406
+
407
+ self.log_text = tk.Text(log_text_frame, height=10, wrap=tk.WORD)
408
+ log_text_scrollbar = ttk.Scrollbar(
409
+ log_text_frame, orient="vertical", command=self.log_text.yview
410
+ )
411
+ self.log_text.configure(yscrollcommand=log_text_scrollbar.set)
412
+
413
+ self.log_text.pack(side="left", fill="both", expand=True)
414
+ log_text_scrollbar.pack(side="right", fill="y")
415
+
416
+ # Log controls
417
+ log_controls = ttk.Frame(log_frame)
418
+ log_controls.pack(fill=tk.X, padx=5, pady=(0, 5))
419
+
420
+ ttk.Button(log_controls, text="Clear Log", command=self._clear_log).pack(side=tk.LEFT)
421
+
422
+ # Log level selector
423
+ ttk.Label(log_controls, text="Log Level:").pack(side=tk.LEFT, padx=(10, 5))
424
+ self.log_level_var = tk.StringVar(value=self.config_manager.app.log_level.default)
425
+ log_level_combo = ttk.Combobox(
426
+ log_controls,
427
+ textvariable=self.log_level_var,
428
+ values=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
429
+ state="readonly",
430
+ width=10,
431
+ )
432
+ log_level_combo.pack(side=tk.LEFT)
433
+ log_level_combo.bind("<<ComboboxSelected>>", self._on_log_level_changed)
434
+
435
+ def _create_menu(self):
436
+ """Create the application menu."""
437
+ menubar = tk.Menu(self.root)
438
+ self.root.config(menu=menubar)
439
+
440
+ # File menu
441
+ file_menu = tk.Menu(menubar, tearoff=0)
442
+ menubar.add_cascade(label="File", menu=file_menu)
443
+ file_menu.add_command(label="Open...", command=self._open_files)
444
+ file_menu.add_separator()
445
+
446
+ # Create Run menu options dynamically
447
+ for mode, label in self.processing_modes:
448
+ file_menu.add_command(label=label, command=partial(self._run_processing, mode=mode))
449
+
450
+ file_menu.add_separator()
451
+ file_menu.add_command(label="Exit", command=self._on_closing)
452
+
453
+ # Options menu
454
+ options_menu = tk.Menu(menubar, tearoff=0)
455
+ menubar.add_cascade(label="Options", menu=options_menu)
456
+ options_menu.add_command(label="Settings", command=self._open_settings)
457
+
458
+ # Help menu
459
+ help_menu = tk.Menu(menubar, tearoff=0)
460
+ menubar.add_cascade(label="Help", menu=help_menu)
461
+ help_menu.add_command(label="User help", command=self._open_help)
462
+ help_menu.add_separator()
463
+ help_menu.add_command(label="About", command=self._show_about)
464
+
465
+ def _setup_gui_logging(self):
466
+ """Setup GUI logging integration."""
467
+ # Create GUI log writer
468
+ self.gui_log_writer = GuiLogWriter(self.log_text)
469
+
470
+ # Connect to logging system
471
+ connect_gui_logging(self.gui_log_writer)
472
+
473
+ def _on_log_level_changed(self, event=None):
474
+ """Handle log level change."""
475
+ new_level = self.log_level_var.get()
476
+ self.logger_manager.set_log_level(new_level)
477
+ self.logger.info(f"Log level changed to {new_level}")
478
+
479
+ def _clear_log(self):
480
+ """Clear the log text widget."""
481
+ self.log_text.delete(1.0, tk.END)
482
+ self.logger.debug("Log display cleared")
483
+
484
+ def _clear_files(self):
485
+ """Clear the file list."""
486
+ self.file_list.clear()
487
+ self.file_listbox.delete(0, tk.END)
488
+ self.logger.info("File list cleared")
489
+
490
+ def _open_files(self):
491
+ """Open file dialog and add files to list."""
492
+ files = filedialog.askopenfilenames(
493
+ title="Select input files", filetypes=[("All files", "*.*"), ("Text files", "*.txt")]
494
+ )
495
+
496
+ new_files = 0
497
+ for file in files:
498
+ if file not in self.file_list:
499
+ self.file_list.append(file)
500
+ self.file_listbox.insert(tk.END, os.path.basename(file))
501
+ new_files += 1
502
+
503
+ if new_files > 0:
504
+ self.logger.info(f"Added {new_files} new files to processing list")
505
+ else:
506
+ self.logger.debug("No new files selected")
507
+
508
+ def _run_processing(self, mode="compress_files"):
509
+ """Run the processing in a separate thread."""
510
+ if not self.file_list:
511
+ self.logger.warning("No input files selected")
512
+ messagebox.showwarning("Warning", "No input files selected!")
513
+ return
514
+
515
+ self.logger.info(f"Starting processing of {len(self.file_list)} files in mode: {mode}")
516
+
517
+ # Alle Buttons während der Verarbeitung deaktivieren
518
+ for button in self.run_buttons.values():
519
+ button.config(state="disabled")
520
+ self.progress.start()
521
+
522
+ # Run in separate thread to avoid blocking GUI
523
+ thread = threading.Thread(target=self._process_files, args=(mode,), daemon=True)
524
+ thread.start()
525
+
526
+ def _process_files(self, mode="compress_files"):
527
+ """Process the selected files."""
528
+ try:
529
+ self.logger.info("=== Processing Started ===")
530
+ self.logger.info("Processing files...")
531
+
532
+ # Create and run project
533
+ project = BaseGPXProcessor(
534
+ self.file_list,
535
+ self.config_manager.cli.output.default,
536
+ self.config_manager.cli.min_dist.default,
537
+ self.config_manager.app.date_format.default,
538
+ self.config_manager.cli.elevation.default,
539
+ self.logger,
540
+ )
541
+ # implement switch case for different processing modes
542
+ if mode == "compress_files":
543
+ project.compress_files()
544
+ elif mode == "merge_files":
545
+ project.merge_files()
546
+ elif mode == "extract_pois":
547
+ project.extract_pois()
548
+ else:
549
+ self.logger.warning(f"Unknown mode: {mode}")
550
+
551
+ self.logger.info(f"Completed: {len(self.file_list)} files processed")
552
+
553
+ self.logger.info("=== All files processed successfully! ===")
554
+
555
+ except Exception as err:
556
+ self.logger.error(f"Processing failed: {err}", exc_info=True)
557
+ # Show error dialog in main thread
558
+ self.root.after(
559
+ 0, lambda e=err: messagebox.showerror("Error", f"Processing failed: {e}")
560
+ )
561
+
562
+ finally:
563
+ # Re-enable controls in main thread
564
+ self.root.after(0, self._processing_finished)
565
+
566
+ def _processing_finished(self):
567
+ """Re-enable controls after processing is finished."""
568
+ for button in self.run_buttons.values():
569
+ button.config(state="normal")
570
+ self.clear_button.config(state="normal")
571
+ self.progress.stop()
572
+
573
+ def _open_settings(self):
574
+ """Open the settings dialog."""
575
+ self.logger.debug("Opening settings dialog")
576
+ dialog = SettingsDialog(self.root, self.config_manager)
577
+ self.root.wait_window(dialog.dialog)
578
+
579
+ if dialog.result == "ok":
580
+ self.logger.info("Settings updated successfully")
581
+ # Update log level selector if it changed
582
+ self.log_level_var.set(self.config_manager.app.log_level.default)
583
+
584
+ def _open_help(self):
585
+ """Open help documentation in browser."""
586
+ self.logger.debug("Opening help documentation")
587
+ webbrowser.open("https://python-template-project.readthedocs.io/en/stable/")
588
+
589
+ def _show_about(self):
590
+ """Show about dialog."""
591
+ self.logger.debug("Showing about dialog")
592
+ messagebox.showinfo("About", "python-template-project\n\nCopyright by Paul")
593
+
594
+ def _on_closing(self):
595
+ """Handle application closing."""
596
+ self.logger.info("Closing GUI application")
597
+ disconnect_gui_logging()
598
+ self.root.quit()
599
+ self.root.destroy()
600
+
601
+
602
+ def main():
603
+ """Main entry point for the GUI application."""
604
+ root = tk.Tk()
605
+ try:
606
+ MainGui(root)
607
+ root.mainloop()
608
+ except Exception as e:
609
+ print(f"GUI startup failed: {e}")
610
+ traceback.print_exc()
611
+ sys.exit(1)
612
+
613
+
614
+ if __name__ == "__main__":
615
+ main()