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.
- python_template_project/__init__.py +0 -0
- python_template_project/__main__.py +8 -0
- python_template_project/cli/__init__.py +0 -0
- python_template_project/cli/__main__.py +8 -0
- python_template_project/cli/cli.py +217 -0
- python_template_project/config/__init__.py +0 -0
- python_template_project/config/config.py +557 -0
- python_template_project/core/__init__.py +0 -0
- python_template_project/core/base.py +397 -0
- python_template_project/core/logging.py +219 -0
- python_template_project/gui/__init__.py +0 -0
- python_template_project/gui/__main__.py +8 -0
- python_template_project/gui/gui.py +615 -0
- python_template_project-0.1.0.dist-info/METADATA +92 -0
- python_template_project-0.1.0.dist-info/RECORD +18 -0
- python_template_project-0.1.0.dist-info/WHEEL +4 -0
- python_template_project-0.1.0.dist-info/entry_points.txt +3 -0
- python_template_project-0.1.0.dist-info/licenses/LICENSE +24 -0
|
@@ -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()
|