config-cli-gui 0.0.2__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.
- __init__.py +0 -0
- config_cli_gui/__init__.py +0 -0
- config_cli_gui/_version.py +21 -0
- config_cli_gui/cli_generator.py +177 -0
- config_cli_gui/config_framework.py +362 -0
- config_cli_gui/gui_generator.py +225 -0
- config_cli_gui-0.0.2.dist-info/METADATA +282 -0
- config_cli_gui-0.0.2.dist-info/RECORD +26 -0
- config_cli_gui-0.0.2.dist-info/WHEEL +5 -0
- config_cli_gui-0.0.2.dist-info/entry_points.txt +3 -0
- config_cli_gui-0.0.2.dist-info/licenses/LICENSE +24 -0
- config_cli_gui-0.0.2.dist-info/top_level.txt +4 -0
- example_project/__init__.py +0 -0
- example_project/__main__.py +8 -0
- example_project/cli/__init__.py +0 -0
- example_project/cli/__main__.py +8 -0
- example_project/cli/cli.py +132 -0
- example_project/config/__init__.py +0 -0
- example_project/config/config.py +209 -0
- example_project/core/__init__.py +0 -0
- example_project/core/base.py +634 -0
- example_project/core/logging.py +219 -0
- example_project/gui/__init__.py +0 -0
- example_project/gui/__main__.py +8 -0
- example_project/gui/gui.py +542 -0
- main.py +153 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""GUI interface for config-cli-gui using tkinter with integrated logging.
|
|
2
|
+
|
|
3
|
+
This module provides a graphical user interface for the config-cli-gui
|
|
4
|
+
with settings dialog, file management, and centralized logging capabilities.
|
|
5
|
+
|
|
6
|
+
run gui: python -m config_cli_gui.gui
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import tkinter as tk
|
|
14
|
+
import traceback
|
|
15
|
+
import webbrowser
|
|
16
|
+
from functools import partial
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from tkinter import filedialog, messagebox, ttk
|
|
19
|
+
|
|
20
|
+
from config_cli_gui.config.config import ConfigParameterManager
|
|
21
|
+
from config_cli_gui.core.base import BaseGPXProcessor
|
|
22
|
+
from config_cli_gui.core.logging import (
|
|
23
|
+
connect_gui_logging,
|
|
24
|
+
disconnect_gui_logging,
|
|
25
|
+
get_logger,
|
|
26
|
+
initialize_logging,
|
|
27
|
+
)
|
|
28
|
+
from config_cli_gui.gui_generator import SettingsDialogGenerator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GuiLogWriter:
|
|
32
|
+
"""Log writer that handles GUI text widget updates in a thread-safe way."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, text_widget):
|
|
35
|
+
self.text_widget = text_widget
|
|
36
|
+
self.root = text_widget.winfo_toplevel()
|
|
37
|
+
self.hyperlink_tags = {} # To store clickable links
|
|
38
|
+
|
|
39
|
+
def write(self, text):
|
|
40
|
+
"""Write text to the widget in a thread-safe manner."""
|
|
41
|
+
# Schedule the GUI update in the main thread
|
|
42
|
+
self.root.after(0, self._update_text, text)
|
|
43
|
+
|
|
44
|
+
def _update_text(self, text):
|
|
45
|
+
"""Update the text widget (must be called from main thread)."""
|
|
46
|
+
try:
|
|
47
|
+
current_end = self.text_widget.index(tk.END)
|
|
48
|
+
self.text_widget.insert(tk.END, text)
|
|
49
|
+
|
|
50
|
+
# Check for a directory path (simplified regex for common path formats)
|
|
51
|
+
# This regex looks for paths that start with a drive letter (C:\), a forward slash (/)
|
|
52
|
+
# or a backslash (\) followed by word characters, and ends with a word character.
|
|
53
|
+
# This is a basic approach; more robust path detection might be needed for edge cases.
|
|
54
|
+
import re
|
|
55
|
+
|
|
56
|
+
path_match = re.search(
|
|
57
|
+
r"([A-Za-z]:[\\/][\S ]*|[\\][\\/][\S ]*|[\w/.-]+[/][\S ]*)\b", text
|
|
58
|
+
)
|
|
59
|
+
if path_match:
|
|
60
|
+
path = path_match.group(0).strip()
|
|
61
|
+
# Ensure the path exists and is a directory to make it clickable
|
|
62
|
+
if Path(path).is_dir():
|
|
63
|
+
start_index = self.text_widget.search(path, current_end, tk.END)
|
|
64
|
+
if start_index:
|
|
65
|
+
end_index = f"{start_index}+{len(path)}c"
|
|
66
|
+
tag_name = f"link_{len(self.hyperlink_tags)}"
|
|
67
|
+
self.text_widget.tag_config(tag_name, foreground="blue", underline=True)
|
|
68
|
+
self.text_widget.tag_bind(
|
|
69
|
+
tag_name, "<Button-1>", lambda e, p=path: self._open_path_in_explorer(p)
|
|
70
|
+
)
|
|
71
|
+
self.text_widget.tag_bind(
|
|
72
|
+
tag_name, "<Enter>", lambda e: self.text_widget.config(cursor="hand2")
|
|
73
|
+
)
|
|
74
|
+
self.text_widget.tag_bind(
|
|
75
|
+
tag_name, "<Leave>", lambda e: self.text_widget.config(cursor="")
|
|
76
|
+
)
|
|
77
|
+
self.text_widget.tag_add(tag_name, start_index, end_index)
|
|
78
|
+
self.hyperlink_tags[tag_name] = path
|
|
79
|
+
|
|
80
|
+
self.text_widget.see(tk.END)
|
|
81
|
+
self.text_widget.update_idletasks()
|
|
82
|
+
except tk.TclError:
|
|
83
|
+
# Widget might be destroyed
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def _open_path_in_explorer(self, path):
|
|
87
|
+
"""Opens the given path in the file explorer."""
|
|
88
|
+
try:
|
|
89
|
+
if sys.platform == "win32":
|
|
90
|
+
os.startfile(path)
|
|
91
|
+
elif sys.platform == "darwin":
|
|
92
|
+
subprocess.Popen(["open", path])
|
|
93
|
+
else:
|
|
94
|
+
subprocess.Popen(["xdg-open", path])
|
|
95
|
+
except Exception as e:
|
|
96
|
+
get_logger("gui.main").error(f"Failed to open path {path}: {e}")
|
|
97
|
+
|
|
98
|
+
def flush(self):
|
|
99
|
+
"""Flush method for compatibility."""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class MainGui:
|
|
104
|
+
"""Main GUI application class."""
|
|
105
|
+
|
|
106
|
+
processing_modes = [
|
|
107
|
+
("compress_files", "Compress"),
|
|
108
|
+
("merge_files", "Merge"),
|
|
109
|
+
("extract_pois", "Extract POIs"),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
def __init__(self, root):
|
|
113
|
+
self.root = root
|
|
114
|
+
self.root.title("config-cli-gui")
|
|
115
|
+
self.root.geometry("1200x600") # Increased width for new layout
|
|
116
|
+
|
|
117
|
+
# Initialize configuration
|
|
118
|
+
self.config_manager = ConfigParameterManager("config.yaml")
|
|
119
|
+
|
|
120
|
+
# Initialize logging system
|
|
121
|
+
self.logger_manager = initialize_logging(self.config_manager)
|
|
122
|
+
self.logger = get_logger("gui.main")
|
|
123
|
+
|
|
124
|
+
# File lists
|
|
125
|
+
self.input_files = []
|
|
126
|
+
self.output_files = []
|
|
127
|
+
|
|
128
|
+
self._build_widgets()
|
|
129
|
+
self._create_menu()
|
|
130
|
+
|
|
131
|
+
# Setup GUI logging after widgets are created
|
|
132
|
+
self._setup_gui_logging()
|
|
133
|
+
|
|
134
|
+
# Handle window closing
|
|
135
|
+
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
|
136
|
+
|
|
137
|
+
self.logger.info("GUI application started")
|
|
138
|
+
self.logger_manager.log_config_summary()
|
|
139
|
+
|
|
140
|
+
def _build_widgets(self):
|
|
141
|
+
"""Build the main GUI widgets."""
|
|
142
|
+
# Main container
|
|
143
|
+
main_frame = ttk.Frame(self.root)
|
|
144
|
+
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|
145
|
+
|
|
146
|
+
# Top frame for file lists and buttons
|
|
147
|
+
top_frame = ttk.Frame(main_frame)
|
|
148
|
+
top_frame.pack(fill=tk.BOTH, expand=True)
|
|
149
|
+
|
|
150
|
+
# Left side - Input File list
|
|
151
|
+
input_file_frame = ttk.LabelFrame(top_frame, text="Input Files")
|
|
152
|
+
input_file_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
|
|
153
|
+
|
|
154
|
+
self.input_file_listbox = tk.Listbox(input_file_frame, selectmode=tk.EXTENDED)
|
|
155
|
+
input_file_scrollbar = ttk.Scrollbar(
|
|
156
|
+
input_file_frame, orient="vertical", command=self.input_file_listbox.yview
|
|
157
|
+
)
|
|
158
|
+
self.input_file_listbox.configure(yscrollcommand=input_file_scrollbar.set)
|
|
159
|
+
|
|
160
|
+
self.input_file_listbox.pack(side="left", fill="both", expand=True, padx=5, pady=5)
|
|
161
|
+
input_file_scrollbar.pack(side="right", fill="y", pady=5)
|
|
162
|
+
self.input_file_listbox.bind(
|
|
163
|
+
"<Double-Button-1>", lambda event: self._open_selected_file(event, self.input_files)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Middle - Output File list
|
|
167
|
+
output_file_frame = ttk.LabelFrame(top_frame, text="Generated Files")
|
|
168
|
+
output_file_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 5))
|
|
169
|
+
|
|
170
|
+
self.output_file_listbox = tk.Listbox(output_file_frame)
|
|
171
|
+
output_file_scrollbar = ttk.Scrollbar(
|
|
172
|
+
output_file_frame, orient="vertical", command=self.output_file_listbox.yview
|
|
173
|
+
)
|
|
174
|
+
self.output_file_listbox.configure(yscrollcommand=output_file_scrollbar.set)
|
|
175
|
+
|
|
176
|
+
self.output_file_listbox.pack(side="left", fill="both", expand=True, padx=5, pady=5)
|
|
177
|
+
output_file_scrollbar.pack(side="right", fill="y", pady=5)
|
|
178
|
+
self.output_file_listbox.bind(
|
|
179
|
+
"<Double-Button-1>", lambda event: self._open_selected_file(event, self.output_files)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Right side - Buttons
|
|
183
|
+
button_frame = ttk.Frame(top_frame)
|
|
184
|
+
button_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(5, 0))
|
|
185
|
+
|
|
186
|
+
open_button = ttk.Button(button_frame, text="Open Files", command=self._open_files)
|
|
187
|
+
open_button.pack(pady=8, fill=tk.X)
|
|
188
|
+
|
|
189
|
+
remove_selected_button = ttk.Button(
|
|
190
|
+
button_frame, text="Remove Selected", command=self._remove_selected_input_files
|
|
191
|
+
)
|
|
192
|
+
remove_selected_button.pack(pady=1, fill=tk.X)
|
|
193
|
+
|
|
194
|
+
# Create buttons dynamically
|
|
195
|
+
self.run_buttons = {}
|
|
196
|
+
for mode, label in self.processing_modes:
|
|
197
|
+
button = ttk.Button(
|
|
198
|
+
button_frame, text=label, command=partial(self._run_processing, mode=mode)
|
|
199
|
+
)
|
|
200
|
+
button.pack(pady=1, fill=tk.X)
|
|
201
|
+
# Save buttons in dictionary for later access
|
|
202
|
+
self.run_buttons[mode] = button
|
|
203
|
+
|
|
204
|
+
# Clear files button
|
|
205
|
+
self.clear_input_button = ttk.Button(
|
|
206
|
+
button_frame, text="Clear Input Files", command=self._clear_input_files
|
|
207
|
+
)
|
|
208
|
+
self.clear_input_button.pack(pady=8, fill=tk.X)
|
|
209
|
+
|
|
210
|
+
self.clear_output_button = ttk.Button(
|
|
211
|
+
button_frame, text="Clear Generated Files", command=self._clear_output_files
|
|
212
|
+
)
|
|
213
|
+
self.clear_output_button.pack(pady=1, fill=tk.X)
|
|
214
|
+
|
|
215
|
+
# Progress bar
|
|
216
|
+
self.progress = ttk.Progressbar(button_frame, mode="indeterminate")
|
|
217
|
+
self.progress.pack(pady=5, fill=tk.X)
|
|
218
|
+
|
|
219
|
+
# Bottom frame - Log output
|
|
220
|
+
log_frame = ttk.LabelFrame(main_frame, text="Log Output")
|
|
221
|
+
log_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
|
|
222
|
+
|
|
223
|
+
# Log text widget with scrollbar
|
|
224
|
+
log_text_frame = ttk.Frame(log_frame)
|
|
225
|
+
log_text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
226
|
+
|
|
227
|
+
self.log_text = tk.Text(log_text_frame, height=10, wrap=tk.WORD)
|
|
228
|
+
log_text_scrollbar = ttk.Scrollbar(
|
|
229
|
+
log_text_frame, orient="vertical", command=self.log_text.yview
|
|
230
|
+
)
|
|
231
|
+
self.log_text.configure(yscrollcommand=log_text_scrollbar.set)
|
|
232
|
+
|
|
233
|
+
self.log_text.pack(side="left", fill="both", expand=True)
|
|
234
|
+
log_text_scrollbar.pack(side="right", fill="y")
|
|
235
|
+
|
|
236
|
+
# Log controls
|
|
237
|
+
log_controls = ttk.Frame(log_frame)
|
|
238
|
+
log_controls.pack(fill=tk.X, padx=5, pady=(0, 5))
|
|
239
|
+
|
|
240
|
+
ttk.Button(log_controls, text="Clear Log", command=self._clear_log).pack(side=tk.LEFT)
|
|
241
|
+
|
|
242
|
+
# Log level selector
|
|
243
|
+
ttk.Label(log_controls, text="Log Level:").pack(side=tk.LEFT, padx=(10, 5))
|
|
244
|
+
self.log_level_var = tk.StringVar(
|
|
245
|
+
value=self.config_manager.get_category("app").log_level.default
|
|
246
|
+
)
|
|
247
|
+
log_level_combo = ttk.Combobox(
|
|
248
|
+
log_controls,
|
|
249
|
+
textvariable=self.log_level_var,
|
|
250
|
+
values=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
251
|
+
state="readonly",
|
|
252
|
+
width=10,
|
|
253
|
+
)
|
|
254
|
+
log_level_combo.pack(side=tk.LEFT)
|
|
255
|
+
log_level_combo.bind("<<ComboboxSelected>>", self._on_log_level_changed)
|
|
256
|
+
|
|
257
|
+
def _create_menu(self):
|
|
258
|
+
"""Create the application menu."""
|
|
259
|
+
menubar = tk.Menu(self.root)
|
|
260
|
+
self.root.config(menu=menubar)
|
|
261
|
+
|
|
262
|
+
# File menu
|
|
263
|
+
file_menu = tk.Menu(menubar, tearoff=0)
|
|
264
|
+
menubar.add_cascade(label="File", menu=file_menu)
|
|
265
|
+
file_menu.add_command(label="Open...", command=self._open_files)
|
|
266
|
+
file_menu.add_separator()
|
|
267
|
+
|
|
268
|
+
# Create Run menu options dynamically
|
|
269
|
+
for mode, label in self.processing_modes:
|
|
270
|
+
file_menu.add_command(label=label, command=partial(self._run_processing, mode=mode))
|
|
271
|
+
|
|
272
|
+
file_menu.add_separator()
|
|
273
|
+
file_menu.add_command(label="Exit", command=self._on_closing)
|
|
274
|
+
|
|
275
|
+
# Options menu
|
|
276
|
+
options_menu = tk.Menu(menubar, tearoff=0)
|
|
277
|
+
menubar.add_cascade(label="Options", menu=options_menu)
|
|
278
|
+
options_menu.add_command(label="Settings", command=self._open_settings)
|
|
279
|
+
|
|
280
|
+
# Help menu
|
|
281
|
+
help_menu = tk.Menu(menubar, tearoff=0)
|
|
282
|
+
menubar.add_cascade(label="Help", menu=help_menu)
|
|
283
|
+
help_menu.add_command(label="User help", command=self._open_help)
|
|
284
|
+
help_menu.add_separator()
|
|
285
|
+
help_menu.add_command(label="About", command=self._show_about)
|
|
286
|
+
|
|
287
|
+
def _setup_gui_logging(self):
|
|
288
|
+
"""Setup GUI logging integration."""
|
|
289
|
+
# Create GUI log writer
|
|
290
|
+
self.gui_log_writer = GuiLogWriter(self.log_text)
|
|
291
|
+
|
|
292
|
+
# Connect to logging system
|
|
293
|
+
connect_gui_logging(self.gui_log_writer)
|
|
294
|
+
|
|
295
|
+
def _on_log_level_changed(self, event=None):
|
|
296
|
+
"""Handle log level change."""
|
|
297
|
+
new_level = self.log_level_var.get()
|
|
298
|
+
self.logger_manager.set_log_level(new_level)
|
|
299
|
+
self.logger.info(f"Log level changed to {new_level}")
|
|
300
|
+
|
|
301
|
+
def _clear_log(self):
|
|
302
|
+
"""Clear the log text widget."""
|
|
303
|
+
self.log_text.delete(1.0, tk.END)
|
|
304
|
+
self.logger.debug("Log display cleared")
|
|
305
|
+
|
|
306
|
+
def _clear_input_files(self):
|
|
307
|
+
"""Clear the input file list."""
|
|
308
|
+
self.input_files.clear()
|
|
309
|
+
self.input_file_listbox.delete(0, tk.END)
|
|
310
|
+
self.logger.info("Input file list cleared")
|
|
311
|
+
|
|
312
|
+
def _clear_output_files(self):
|
|
313
|
+
"""Clear the output file list."""
|
|
314
|
+
self.output_files.clear()
|
|
315
|
+
self.output_file_listbox.delete(0, tk.END)
|
|
316
|
+
self.logger.info("Generated file list cleared")
|
|
317
|
+
|
|
318
|
+
def _remove_selected_input_files(self):
|
|
319
|
+
"""Remove selected files from the input file list."""
|
|
320
|
+
selected_indices = self.input_file_listbox.curselection()
|
|
321
|
+
if not selected_indices:
|
|
322
|
+
messagebox.showwarning("Warning", "No files selected to remove!")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# Delete from listbox from end to start to avoid index issues
|
|
326
|
+
for i in reversed(selected_indices):
|
|
327
|
+
self.input_file_listbox.delete(i)
|
|
328
|
+
del self.input_files[i]
|
|
329
|
+
self.logger.info(f"Removed {len(selected_indices)} selected input files.")
|
|
330
|
+
|
|
331
|
+
def _open_selected_file(self, event, file_list_source):
|
|
332
|
+
"""Opens the selected file in the system's default application or explorer."""
|
|
333
|
+
selection_index = event.widget.nearest(event.y)
|
|
334
|
+
if selection_index == -1: # No item clicked
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
file_path_str = file_list_source[selection_index]["path"]
|
|
338
|
+
file_path = Path(file_path_str)
|
|
339
|
+
|
|
340
|
+
if not file_path.exists():
|
|
341
|
+
self.logger.error(f"File not found: {file_path}")
|
|
342
|
+
messagebox.showerror("Error", f"File not found: {file_path}")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
if sys.platform == "win32":
|
|
347
|
+
os.startfile(file_path)
|
|
348
|
+
elif sys.platform == "darwin":
|
|
349
|
+
subprocess.Popen(["open", file_path])
|
|
350
|
+
else:
|
|
351
|
+
subprocess.Popen(["xdg-open", file_path])
|
|
352
|
+
self.logger.info(f"Opened file: {file_path}")
|
|
353
|
+
except Exception as e:
|
|
354
|
+
self.logger.error(f"Could not open file {file_path}: {e}")
|
|
355
|
+
messagebox.showerror("Error", f"Could not open file {file_path}: {e}")
|
|
356
|
+
|
|
357
|
+
def _open_files(self):
|
|
358
|
+
"""Open file dialog and add files to list."""
|
|
359
|
+
files = filedialog.askopenfilenames(
|
|
360
|
+
title="Select input files",
|
|
361
|
+
filetypes=[
|
|
362
|
+
("GPX/KML files", "*.gpx *.kml *.zip"), # Added KML support
|
|
363
|
+
("GPX files", "*.gpx"),
|
|
364
|
+
("KML files", "*.kml"),
|
|
365
|
+
("ZIP files", "*.zip"),
|
|
366
|
+
("All files", "*.*"),
|
|
367
|
+
],
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
new_files = 0
|
|
371
|
+
for file_path_str in files:
|
|
372
|
+
file_path = Path(file_path_str)
|
|
373
|
+
if file_path_str not in [f["path"] for f in self.input_files]:
|
|
374
|
+
try:
|
|
375
|
+
file_size_kb = file_path.stat().st_size / 1024
|
|
376
|
+
self.input_files.append({"path": file_path_str, "size": file_size_kb})
|
|
377
|
+
self.input_file_listbox.insert(
|
|
378
|
+
tk.END, f"{file_path.name} ({file_size_kb:.2f} KB)"
|
|
379
|
+
)
|
|
380
|
+
new_files += 1
|
|
381
|
+
except Exception as e:
|
|
382
|
+
self.logger.warning(f"Could not get size for {file_path_str}: {e}")
|
|
383
|
+
self.input_files.append({"path": file_path_str, "size": 0})
|
|
384
|
+
self.input_file_listbox.insert(tk.END, f"{file_path.name} (N/A KB)")
|
|
385
|
+
|
|
386
|
+
if new_files > 0:
|
|
387
|
+
self.logger.info(f"Added {new_files} new files to processing list")
|
|
388
|
+
else:
|
|
389
|
+
self.logger.debug("No new files selected")
|
|
390
|
+
|
|
391
|
+
def _update_output_listbox(self, generated_files_info):
|
|
392
|
+
"""Updates the output file listbox with newly generated files."""
|
|
393
|
+
self.output_file_listbox.delete(0, tk.END) # Clear current list
|
|
394
|
+
self.output_files.clear() # Clear internal list
|
|
395
|
+
for file_path_str in generated_files_info:
|
|
396
|
+
file_path = Path(file_path_str)
|
|
397
|
+
try:
|
|
398
|
+
file_size_kb = file_path.stat().st_size / 1024
|
|
399
|
+
self.output_files.append({"path": file_path_str, "size": file_size_kb})
|
|
400
|
+
self.output_file_listbox.insert(tk.END, f"{file_path.name} ({file_size_kb:.2f} KB)")
|
|
401
|
+
except Exception as e:
|
|
402
|
+
self.logger.warning(f"Could not get size for generated file {file_path_str}: {e}")
|
|
403
|
+
self.output_files.append({"path": file_path_str, "size": 0})
|
|
404
|
+
self.output_file_listbox.insert(tk.END, f"{file_path.name} (N/A KB)")
|
|
405
|
+
|
|
406
|
+
if generated_files_info:
|
|
407
|
+
output_dir = Path(generated_files_info[0]).parent
|
|
408
|
+
self.logger.info(f"Generated files saved in: {output_dir}") # Log directory
|
|
409
|
+
|
|
410
|
+
def _run_processing(self, mode="compress_files"):
|
|
411
|
+
"""Run the processing in a separate thread."""
|
|
412
|
+
selected_indices = self.input_file_listbox.curselection()
|
|
413
|
+
files_to_process = []
|
|
414
|
+
|
|
415
|
+
if selected_indices:
|
|
416
|
+
for i in selected_indices:
|
|
417
|
+
files_to_process.append(self.input_files[i]["path"])
|
|
418
|
+
else:
|
|
419
|
+
files_to_process = [f["path"] for f in self.input_files]
|
|
420
|
+
|
|
421
|
+
if not files_to_process:
|
|
422
|
+
self.logger.warning("No input files selected or all are deselected.")
|
|
423
|
+
messagebox.showwarning("Warning", "No input files selected or all are deselected!")
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
self.logger.info(f"Starting processing of {len(files_to_process)} files in mode: {mode}")
|
|
427
|
+
|
|
428
|
+
# Disable all buttons during processing
|
|
429
|
+
for button in self.run_buttons.values():
|
|
430
|
+
button.config(state="disabled")
|
|
431
|
+
self.clear_input_button.config(state="disabled")
|
|
432
|
+
self.clear_output_button.config(state="disabled")
|
|
433
|
+
self.progress.start()
|
|
434
|
+
|
|
435
|
+
# Run in separate thread to avoid blocking GUI
|
|
436
|
+
thread = threading.Thread(
|
|
437
|
+
target=self._process_files,
|
|
438
|
+
args=(
|
|
439
|
+
mode,
|
|
440
|
+
files_to_process,
|
|
441
|
+
),
|
|
442
|
+
daemon=True,
|
|
443
|
+
)
|
|
444
|
+
thread.start()
|
|
445
|
+
|
|
446
|
+
def _process_files(self, mode="compress_files", files_to_process=None):
|
|
447
|
+
"""Process the selected files."""
|
|
448
|
+
generated_files_paths = []
|
|
449
|
+
try:
|
|
450
|
+
self.logger.info("=== Processing Started ===")
|
|
451
|
+
self.logger.info("Processing files...")
|
|
452
|
+
|
|
453
|
+
if files_to_process is None:
|
|
454
|
+
files_to_process = [] # Should not happen with the check in _run_processing
|
|
455
|
+
|
|
456
|
+
# Create and run project
|
|
457
|
+
project = BaseGPXProcessor(
|
|
458
|
+
files_to_process, # Pass selected files
|
|
459
|
+
self.config_manager.get_category("cli").output.default,
|
|
460
|
+
self.config_manager.get_category("cli").min_dist.default,
|
|
461
|
+
self.config_manager.get_category("app").date_format.default,
|
|
462
|
+
self.config_manager.get_category("cli").elevation.default,
|
|
463
|
+
self.logger,
|
|
464
|
+
)
|
|
465
|
+
# implement switch case for different processing modes
|
|
466
|
+
if mode == "compress_files":
|
|
467
|
+
generated_files_paths = project.compress_files()
|
|
468
|
+
elif mode == "merge_files":
|
|
469
|
+
generated_files_paths = project.merge_files()
|
|
470
|
+
elif mode == "extract_pois":
|
|
471
|
+
generated_files_paths = project.extract_pois()
|
|
472
|
+
else:
|
|
473
|
+
self.logger.warning(f"Unknown mode: {mode}")
|
|
474
|
+
|
|
475
|
+
self.logger.info(f"Completed: {len(files_to_process)} files processed")
|
|
476
|
+
self.logger.info("=== All files processed successfully! ===")
|
|
477
|
+
|
|
478
|
+
self.root.after(0, self._update_output_listbox, generated_files_paths)
|
|
479
|
+
|
|
480
|
+
except Exception as err:
|
|
481
|
+
self.logger.error(f"Processing failed: {err}", exc_info=True)
|
|
482
|
+
# Show error dialog in main thread
|
|
483
|
+
self.root.after(
|
|
484
|
+
0, lambda e=err: messagebox.showerror("Error", f"Processing failed: {e}")
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
finally:
|
|
488
|
+
# Re-enable controls in main thread
|
|
489
|
+
self.root.after(0, self._processing_finished)
|
|
490
|
+
|
|
491
|
+
def _processing_finished(self):
|
|
492
|
+
"""Re-enable controls after processing is finished."""
|
|
493
|
+
for button in self.run_buttons.values():
|
|
494
|
+
button.config(state="normal")
|
|
495
|
+
self.clear_input_button.config(state="normal")
|
|
496
|
+
self.clear_output_button.config(state="normal")
|
|
497
|
+
self.progress.stop()
|
|
498
|
+
|
|
499
|
+
def _open_settings(self):
|
|
500
|
+
"""Open the settings dialog."""
|
|
501
|
+
self.logger.debug("Opening settings dialog")
|
|
502
|
+
settings_dialog_generator = SettingsDialogGenerator(self.config_manager)
|
|
503
|
+
dialog = settings_dialog_generator.create_settings_dialog(self.root)
|
|
504
|
+
self.root.wait_window(dialog.dialog)
|
|
505
|
+
|
|
506
|
+
if dialog.result == "ok":
|
|
507
|
+
self.logger.info("Settings updated successfully")
|
|
508
|
+
# Update log level selector if it changed
|
|
509
|
+
self.log_level_var.set(self.config_manager.get_category("app").log_level.default)
|
|
510
|
+
|
|
511
|
+
def _open_help(self):
|
|
512
|
+
"""Open help documentation in browser."""
|
|
513
|
+
self.logger.debug("Opening help documentation")
|
|
514
|
+
webbrowser.open("https://config-cli-gui.readthedocs.io/en/stable/")
|
|
515
|
+
|
|
516
|
+
def _show_about(self):
|
|
517
|
+
"""Show about dialog."""
|
|
518
|
+
self.logger.debug("Showing about dialog")
|
|
519
|
+
messagebox.showinfo("About", "config-cli-gui\n\nCopyright by Paul")
|
|
520
|
+
|
|
521
|
+
def _on_closing(self):
|
|
522
|
+
"""Handle application closing."""
|
|
523
|
+
self.logger.info("Closing GUI application")
|
|
524
|
+
disconnect_gui_logging()
|
|
525
|
+
self.root.quit()
|
|
526
|
+
self.root.destroy()
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def main():
|
|
530
|
+
"""Main entry point for the GUI application."""
|
|
531
|
+
root = tk.Tk()
|
|
532
|
+
try:
|
|
533
|
+
MainGui(root)
|
|
534
|
+
root.mainloop()
|
|
535
|
+
except Exception as e:
|
|
536
|
+
print(f"GUI startup failed: {e}")
|
|
537
|
+
traceback.print_exc()
|
|
538
|
+
sys.exit(1)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
if __name__ == "__main__":
|
|
542
|
+
main()
|
main.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unified entry point for CLI and GUI application.
|
|
4
|
+
Automatically detects whether to run CLI or GUI based on how the application is started.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_console_attached():
|
|
12
|
+
"""
|
|
13
|
+
Check if the application is running in a console environment.
|
|
14
|
+
Returns True if launched from console, False if launched via double-click.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
# On Windows, check if we have a console attached
|
|
18
|
+
if os.name == "nt":
|
|
19
|
+
import ctypes
|
|
20
|
+
|
|
21
|
+
kernel32 = ctypes.windll.kernel32
|
|
22
|
+
# GetConsoleWindow returns 0 if no console is attached
|
|
23
|
+
console_window = kernel32.GetConsoleWindow()
|
|
24
|
+
if console_window == 0:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
# Check if the console was created by this process
|
|
28
|
+
# (indicates double-click launch with console auto-created)
|
|
29
|
+
process_list = kernel32.GetConsoleProcessList(None, 0)
|
|
30
|
+
if process_list <= 1:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
return True
|
|
34
|
+
else:
|
|
35
|
+
# On Unix-like systems (macOS, Linux), check multiple indicators
|
|
36
|
+
# Check if stdout is a terminal
|
|
37
|
+
if not sys.stdout.isatty():
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
# Additional check for macOS: Check if we're running in a .app bundle
|
|
41
|
+
if sys.platform == "darwin":
|
|
42
|
+
# If we're in a .app bundle, we're likely launched via double-click
|
|
43
|
+
executable_path = sys.executable
|
|
44
|
+
if ".app/" in executable_path:
|
|
45
|
+
# We're in an app bundle, check if we have a real terminal
|
|
46
|
+
# by checking if TERM environment variable is set
|
|
47
|
+
return os.environ.get("TERM") is not None
|
|
48
|
+
|
|
49
|
+
return True
|
|
50
|
+
except Exception as ex:
|
|
51
|
+
# Fallback: assume GUI if we can't determine
|
|
52
|
+
print(ex)
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def has_command_line_args():
|
|
57
|
+
"""Check if command line arguments (beyond script name) are provided."""
|
|
58
|
+
return len(sys.argv) > 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
"""Main entry point that decides between CLI and GUI."""
|
|
63
|
+
|
|
64
|
+
# Force CLI mode if command line arguments are provided
|
|
65
|
+
if has_command_line_args():
|
|
66
|
+
run_cli()
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Check if we're in a console environment
|
|
70
|
+
if is_console_attached():
|
|
71
|
+
# We're in a console - offer choice or default to CLI
|
|
72
|
+
print("Python Template Project")
|
|
73
|
+
print("=" * 50)
|
|
74
|
+
print("Detected console environment.")
|
|
75
|
+
print("Options:")
|
|
76
|
+
print(" 1. Run CLI interface (default)")
|
|
77
|
+
print(" 2. Run GUI interface")
|
|
78
|
+
print(" 3. Show help")
|
|
79
|
+
print()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
choice = input("Select option [1]: ").strip()
|
|
83
|
+
if choice == "2":
|
|
84
|
+
run_gui()
|
|
85
|
+
elif choice == "3":
|
|
86
|
+
show_help()
|
|
87
|
+
else:
|
|
88
|
+
run_cli()
|
|
89
|
+
except (KeyboardInterrupt, EOFError):
|
|
90
|
+
print("\nExiting...")
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
else:
|
|
93
|
+
# Launched via double-click - start GUI
|
|
94
|
+
run_gui()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_cli():
|
|
98
|
+
"""Launch the CLI interface."""
|
|
99
|
+
try:
|
|
100
|
+
from config_cli_gui.cli.cli import main as cli_main
|
|
101
|
+
|
|
102
|
+
cli_main()
|
|
103
|
+
except ImportError as e:
|
|
104
|
+
print(f"Error importing CLI module: {e}")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error running CLI: {e}")
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run_gui():
|
|
112
|
+
"""Launch the GUI interface."""
|
|
113
|
+
try:
|
|
114
|
+
from config_cli_gui.gui.gui import main as gui_main
|
|
115
|
+
|
|
116
|
+
gui_main()
|
|
117
|
+
except ImportError as e:
|
|
118
|
+
print(f"Error importing GUI module: {e}")
|
|
119
|
+
# If GUI fails, fallback to CLI
|
|
120
|
+
print("Falling back to CLI interface...")
|
|
121
|
+
run_cli()
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"Error running GUI: {e}")
|
|
124
|
+
print("Falling back to CLI interface...")
|
|
125
|
+
run_cli()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def show_help():
|
|
129
|
+
"""Show help information."""
|
|
130
|
+
print("""
|
|
131
|
+
Unified Application
|
|
132
|
+
|
|
133
|
+
Usage:
|
|
134
|
+
When launched from console:
|
|
135
|
+
- Without arguments: Interactive mode selection
|
|
136
|
+
- With arguments: Direct CLI mode
|
|
137
|
+
|
|
138
|
+
When launched via double-click:
|
|
139
|
+
- Automatically starts GUI interface
|
|
140
|
+
|
|
141
|
+
Command Line Arguments:
|
|
142
|
+
Run with any CLI arguments to force CLI mode.
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
python main.py --help # Shows CLI help
|
|
146
|
+
python main.py # Interactive mode selection
|
|
147
|
+
double-click main.exe # Starts GUI
|
|
148
|
+
|
|
149
|
+
""")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
main()
|