13
13
import argparse
14
14
import tkinter as tk
15
15
from logging import basicConfig as logging_basicConfig
16
- from logging import debug as logging_debug
17
16
from logging import getLevelName as logging_getLevelName
17
+ from logging import info as logging_info
18
+ from tkinter import font as tkfont
18
19
from tkinter import ttk
19
20
from typing import Optional
20
21
@@ -39,14 +40,20 @@ class TemplateOverviewWindow(BaseWindow):
39
40
40
41
Attributes:
41
42
window (tk.Tk|None): The root Tkinter window object for the GUI.
42
-
43
- Methods:
44
- on_row_double_click(event): Handles the event triggered when a row in the Treeview is double-clicked, allowing the user
45
- to store the corresponding template directory.
43
+ sort_column (str): The column currently being used for sorting
44
+ tree (ttk.Treeview): The treeview widget displaying templates
45
+ image_label (ttk.Label): Label for displaying vehicle images
46
46
47
47
"""
48
48
49
49
def __init__ (self , parent : Optional [tk .Tk ] = None ) -> None :
50
+ """
51
+ Initialize the TemplateOverviewWindow.
52
+
53
+ Args:
54
+ parent: Optional parent Tk window
55
+
56
+ """
50
57
super ().__init__ (parent )
51
58
title = _ ("Amilcar Lucas's - ArduPilot methodic configurator {} - Template Overview and selection" )
52
59
self .root .title (title .format (__version__ ))
@@ -63,8 +70,24 @@ def __init__(self, parent: Optional[tk.Tk] = None) -> None:
63
70
self .image_label = ttk .Label (self .top_frame )
64
71
self .image_label .pack (side = tk .RIGHT , anchor = tk .NE , padx = (20 , 20 ), pady = IMAGE_HEIGHT_PX / 2 )
65
72
66
- self .sort_column : str
73
+ self .sort_column : str = ""
74
+ self ._setup_treeview ()
75
+ self ._bind_events ()
76
+
77
+ def run_app (self ) -> None :
78
+ """Run the TemplateOverviewWindow application."""
79
+ if isinstance (self .root , tk .Toplevel ):
80
+ try :
81
+ while self .root .children :
82
+ self .root .update_idletasks ()
83
+ self .root .update ()
84
+ except tk .TclError as _exp :
85
+ pass
86
+ elif isinstance (self .root , tk .Tk ):
87
+ self .root .mainloop ()
67
88
89
+ def _setup_treeview (self ) -> None :
90
+ """Set up the treeview with columns and styling."""
68
91
style = ttk .Style (self .root )
69
92
# Add padding to Treeview heading style
70
93
style .layout (
@@ -99,76 +122,87 @@ def __init__(self, parent: Optional[tk.Tk] = None) -> None:
99
122
for col in columns :
100
123
self .tree .heading (col , text = col )
101
124
102
- # Populate the Treeview with data from the template overview
125
+ self ._populate_treeview ()
126
+ self ._adjust_treeview_column_widths ()
127
+ self .tree .pack (fill = tk .BOTH , expand = True )
128
+
129
+ def _populate_treeview (self ) -> None :
130
+ """Populate the treeview with data from vehicle components."""
103
131
for key , template_overview in VehicleComponents .get_vehicle_components_overviews ().items ():
104
132
attribute_names = template_overview .attributes ()
105
133
values = (key , * (getattr (template_overview , attr , "" ) for attr in attribute_names ))
106
134
self .tree .insert ("" , "end" , text = key , values = values )
107
135
108
- self ._adjust_treeview_column_widths ()
109
-
110
- self .tree .bind ("<ButtonRelease-1>" , self .__on_row_selection_change )
111
- self .tree .bind ("<Up>" , self .__on_row_selection_change )
112
- self .tree .bind ("<Down>" , self .__on_row_selection_change )
113
- self .tree .bind ("<Double-1>" , self .__on_row_double_click )
114
- self .tree .pack (fill = tk .BOTH , expand = True )
115
-
116
- for col in self .tree ["columns" ]:
117
- col_str = str (col )
118
- self .tree .heading (
119
- col_str ,
120
- text = col_str ,
121
- command = lambda col2 = col_str : self .__sort_by_column (col2 , reverse = False ), # type: ignore[misc]
122
- )
123
-
124
- if isinstance (self .root , tk .Toplevel ):
125
- try :
126
- while self .root .children :
127
- self .root .update_idletasks ()
128
- self .root .update ()
129
- except tk .TclError as _exp :
130
- pass
131
- else :
132
- self .root .mainloop ()
133
-
134
136
def _adjust_treeview_column_widths (self ) -> None :
135
137
"""Adjusts the column widths of the Treeview to fit the contents of each column."""
136
138
for col in self .tree ["columns" ]:
137
139
max_width = 0
138
140
for subtitle in col .title ().split ("\n " ):
139
- max_width = max (max_width , tk . font . Font ().measure (subtitle )) # pyright: ignore[reportAttributeAccessIssue]
141
+ max_width = max (max_width , tkfont . Font ().measure (subtitle ))
140
142
141
143
# Iterate over all rows and update the max_width if a wider entry is found
142
144
for item in self .tree .get_children ():
143
145
item_text = self .tree .item (item , "values" )[self .tree ["columns" ].index (col )]
144
- text_width = tk . font . Font ().measure (item_text ) # pyright: ignore[reportAttributeAccessIssue]
146
+ text_width = tkfont . Font ().measure (item_text )
145
147
max_width = max (max_width , text_width )
146
148
147
149
# Update the column's width property to accommodate the largest text width
148
150
self .tree .column (col , width = int (max_width * 0.6 + 10 ))
149
151
150
- def __on_row_selection_change (self , _event : tk .Event ) -> None :
152
+ def _bind_events (self ) -> None :
153
+ """Bind events to the treeview."""
154
+ self .tree .bind ("<ButtonRelease-1>" , self ._on_row_selection_change )
155
+ self .tree .bind ("<Up>" , self ._on_row_selection_change )
156
+ self .tree .bind ("<Down>" , self ._on_row_selection_change )
157
+ self .tree .bind ("<Double-1>" , self ._on_row_double_click )
158
+
159
+ for col in self .tree ["columns" ]:
160
+ col_str = str (col )
161
+ self .tree .heading (
162
+ col_str ,
163
+ text = col_str ,
164
+ command = lambda col2 = col_str : self ._sort_by_column (col2 , reverse = False ), # type: ignore[misc]
165
+ )
166
+
167
+ def _on_row_selection_change (self , _event : tk .Event ) -> None :
151
168
"""Handle row single-click event."""
152
- self .root .after (0 , self .__update_selection )
169
+ self .root .after (0 , self ._update_selection )
153
170
154
- def __update_selection (self ) -> None :
171
+ def _update_selection (self ) -> None :
155
172
"""Update selection after keypress event."""
156
173
selected_item = self .tree .selection ()
157
174
if selected_item :
158
175
item_id = selected_item [0 ]
159
176
selected_template_relative_path = self .tree .item (item_id )["text" ]
160
- ProgramSettings .store_template_dir (selected_template_relative_path )
177
+ self .store_template_dir (selected_template_relative_path )
161
178
self ._display_vehicle_image (selected_template_relative_path )
162
179
163
- def __on_row_double_click (self , event : tk .Event ) -> None :
180
+ def store_template_dir (self , template_path : str ) -> None :
181
+ """
182
+ Store the selected template directory.
183
+
184
+ This method is separated from the UI event handler to improve testability.
185
+
186
+ Args:
187
+ template_path: The path to store
188
+
189
+ """
190
+ ProgramSettings .store_template_dir (template_path )
191
+
192
+ def _on_row_double_click (self , event : tk .Event ) -> None :
164
193
"""Handle row double-click event."""
165
194
item_id = self .tree .identify_row (event .y )
166
195
if item_id :
167
196
selected_template_relative_path = self .tree .item (item_id )["text" ]
168
- ProgramSettings .store_template_dir (selected_template_relative_path )
169
- self .root .destroy ()
197
+ self .store_template_dir (selected_template_relative_path )
198
+ self .close_window ()
199
+
200
+ def close_window (self ) -> None :
201
+ """Close the window - separated for testability."""
202
+ self .root .destroy ()
170
203
171
- def __sort_by_column (self , col : str , reverse : bool ) -> None :
204
+ def _sort_by_column (self , col : str , reverse : bool ) -> None :
205
+ """Sort treeview items by the specified column."""
172
206
if hasattr (self , "sort_column" ) and self .sort_column and self .sort_column != col :
173
207
self .tree .heading (self .sort_column , text = self .sort_column )
174
208
self .tree .heading (col , text = col + (" ▼" if reverse else " ▲" ))
@@ -185,7 +219,7 @@ def __sort_by_column(self, col: str, reverse: bool) -> None:
185
219
self .tree .move (k , "" , index )
186
220
187
221
# reverse sort next time
188
- self .tree .heading (col , command = lambda : self .__sort_by_column (col , not reverse ))
222
+ self .tree .heading (col , command = lambda : self ._sort_by_column (col , not reverse ))
189
223
190
224
def _display_vehicle_image (self , template_path : str ) -> None :
191
225
"""Display the vehicle image corresponding to the selected template."""
@@ -194,7 +228,7 @@ def _display_vehicle_image(self, template_path: str) -> None:
194
228
if isinstance (widget , ttk .Label ) and widget == self .image_label :
195
229
widget .destroy ()
196
230
try :
197
- vehicle_image_filepath = VehicleComponents .get_vehicle_image_filepath (template_path )
231
+ vehicle_image_filepath = self .get_vehicle_image_filepath (template_path )
198
232
self .image_label = self .put_image_in_label (self .top_frame , vehicle_image_filepath , IMAGE_HEIGHT_PX )
199
233
except FileNotFoundError :
200
234
self .image_label = ttk .Label (
@@ -204,6 +238,24 @@ def _display_vehicle_image(self, template_path: str) -> None:
204
238
)
205
239
self .image_label .pack (side = tk .RIGHT , anchor = tk .NE , padx = (4 , 0 ), pady = (0 , 0 ))
206
240
241
+ def get_vehicle_image_filepath (self , template_path : str ) -> str :
242
+ """
243
+ Get the filepath for a vehicle image.
244
+
245
+ Separated from display method for testability.
246
+
247
+ Args:
248
+ template_path: Path to the template
249
+
250
+ Returns:
251
+ Path to the vehicle image
252
+
253
+ Raises:
254
+ FileNotFoundError: If the image file doesn't exist
255
+
256
+ """
257
+ return VehicleComponents .get_vehicle_image_filepath (template_path )
258
+
207
259
208
260
def argument_parser () -> argparse .Namespace :
209
261
"""
@@ -217,25 +269,38 @@ def argument_parser() -> argparse.Namespace:
217
269
"""
218
270
parser = argparse .ArgumentParser (
219
271
description = _ (
220
- "ArduPilot methodic configurator is a GUI-based tool designed to simplify "
221
- "the management and visualization of ArduPilot parameters. It enables users "
222
- "to browse through various vehicle templates, edit parameter files, and "
223
- "apply changes directly to the flight controller. The tool is built to "
224
- "semi-automate the configuration process of ArduPilot for drones by "
225
- "providing a clear and intuitive interface for parameter management ."
272
+ "ArduPilot Template Overview - A component of the ArduPilot Methodic Configurator suite. "
273
+ "This tool presents available vehicle templates in a user-friendly interface, allowing you "
274
+ "to browse, compare, and select the most appropriate template for your vehicle configuration. "
275
+ "Select a template that most closely resembles your vehicle's component setup to streamline "
276
+ "the configuration process. The selected template will serve as a starting point for more "
277
+ "detailed parameter configuration ."
226
278
)
227
279
)
228
280
return add_common_arguments (parser ).parse_args ()
229
281
230
282
283
+ def setup_logging (loglevel : str ) -> None :
284
+ """
285
+ Set up logging with the specified log level.
286
+
287
+ Args:
288
+ loglevel: The log level as a string (e.g. 'DEBUG', 'INFO')
289
+
290
+ """
291
+ logging_basicConfig (level = logging_getLevelName (loglevel ), format = "%(asctime)s - %(levelname)s - %(message)s" )
292
+
293
+
231
294
def main () -> None :
295
+ """Main entry point for the application."""
232
296
args = argument_parser ()
297
+ setup_logging (args .loglevel )
233
298
234
- logging_basicConfig (level = logging_getLevelName (args .loglevel ), format = "%(asctime)s - %(levelname)s - %(message)s" )
235
-
236
- TemplateOverviewWindow (None )
299
+ window = TemplateOverviewWindow ()
300
+ window .run_app ()
237
301
238
- logging_debug (ProgramSettings .get_recently_used_dirs ()[0 ])
302
+ if window and ProgramSettings .get_recently_used_dirs ():
303
+ logging_info (ProgramSettings .get_recently_used_dirs ()[0 ])
239
304
240
305
241
306
if __name__ == "__main__" :
0 commit comments