1313import argparse
1414import tkinter as tk
1515from logging import basicConfig as logging_basicConfig
16- from logging import debug as logging_debug
1716from logging import getLevelName as logging_getLevelName
17+ from logging import info as logging_info
18+ from tkinter import font as tkfont
1819from tkinter import ttk
1920from typing import Optional
2021
@@ -39,14 +40,20 @@ class TemplateOverviewWindow(BaseWindow):
3940
4041 Attributes:
4142 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
4646
4747 """
4848
4949 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+ """
5057 super ().__init__ (parent )
5158 title = _ ("Amilcar Lucas's - ArduPilot methodic configurator {} - Template Overview and selection" )
5259 self .root .title (title .format (__version__ ))
@@ -63,8 +70,12 @@ def __init__(self, parent: Optional[tk.Tk] = None) -> None:
6370 self .image_label = ttk .Label (self .top_frame )
6471 self .image_label .pack (side = tk .RIGHT , anchor = tk .NE , padx = (20 , 20 ), pady = IMAGE_HEIGHT_PX / 2 )
6572
66- self .sort_column : str
73+ self .sort_column : str = ""
74+ self .setup_treeview ()
75+ self .bind_events ()
6776
77+ def setup_treeview (self ) -> None :
78+ """Set up the treeview with columns and styling."""
6879 style = ttk .Style (self .root )
6980 # Add padding to Treeview heading style
7081 style .layout (
@@ -99,76 +110,87 @@ def __init__(self, parent: Optional[tk.Tk] = None) -> None:
99110 for col in columns :
100111 self .tree .heading (col , text = col )
101112
102- # Populate the Treeview with data from the template overview
113+ self .populate_treeview ()
114+ self ._adjust_treeview_column_widths ()
115+ self .tree .pack (fill = tk .BOTH , expand = True )
116+
117+ def populate_treeview (self ) -> None :
118+ """Populate the treeview with data from vehicle components."""
103119 for key , template_overview in VehicleComponents .get_vehicle_components_overviews ().items ():
104120 attribute_names = template_overview .attributes ()
105121 values = (key , * (getattr (template_overview , attr , "" ) for attr in attribute_names ))
106122 self .tree .insert ("" , "end" , text = key , values = values )
107123
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 )
124+ def bind_events (self ) -> None :
125+ """Bind events to the treeview."""
126+ self .tree .bind ("<ButtonRelease-1>" , self ._on_row_selection_change )
127+ self .tree .bind ("<Up>" , self ._on_row_selection_change )
128+ self .tree .bind ("<Down>" , self ._on_row_selection_change )
129+ self .tree .bind ("<Double-1>" , self ._on_row_double_click )
115130
116131 for col in self .tree ["columns" ]:
117132 col_str = str (col )
118133 self .tree .heading (
119134 col_str ,
120135 text = col_str ,
121- command = lambda col2 = col_str : self .__sort_by_column (col2 , reverse = False ), # type: ignore[misc]
136+ command = lambda col2 = col_str : self ._sort_by_column (col2 , reverse = False ), # type: ignore[misc]
122137 )
123138
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-
134139 def _adjust_treeview_column_widths (self ) -> None :
135140 """Adjusts the column widths of the Treeview to fit the contents of each column."""
136141 for col in self .tree ["columns" ]:
137142 max_width = 0
138143 for subtitle in col .title ().split ("\n " ):
139- max_width = max (max_width , tk . font . Font ().measure (subtitle )) # pyright: ignore[reportAttributeAccessIssue]
144+ max_width = max (max_width , tkfont . Font ().measure (subtitle ))
140145
141146 # Iterate over all rows and update the max_width if a wider entry is found
142147 for item in self .tree .get_children ():
143148 item_text = self .tree .item (item , "values" )[self .tree ["columns" ].index (col )]
144- text_width = tk . font . Font ().measure (item_text ) # pyright: ignore[reportAttributeAccessIssue]
149+ text_width = tkfont . Font ().measure (item_text )
145150 max_width = max (max_width , text_width )
146151
147152 # Update the column's width property to accommodate the largest text width
148153 self .tree .column (col , width = int (max_width * 0.6 + 10 ))
149154
150- def __on_row_selection_change (self , _event : tk .Event ) -> None :
155+ def _on_row_selection_change (self , _event : tk .Event ) -> None :
151156 """Handle row single-click event."""
152- self .root .after (0 , self .__update_selection )
157+ self .root .after (0 , self ._update_selection )
153158
154- def __update_selection (self ) -> None :
159+ def _update_selection (self ) -> None :
155160 """Update selection after keypress event."""
156161 selected_item = self .tree .selection ()
157162 if selected_item :
158163 item_id = selected_item [0 ]
159164 selected_template_relative_path = self .tree .item (item_id )["text" ]
160- ProgramSettings .store_template_dir (selected_template_relative_path )
165+ self .store_template_dir (selected_template_relative_path )
161166 self ._display_vehicle_image (selected_template_relative_path )
162167
163- def __on_row_double_click (self , event : tk .Event ) -> None :
168+ def store_template_dir (self , template_path : str ) -> None :
169+ """
170+ Store the selected template directory.
171+
172+ This method is separated from the UI event handler to improve testability.
173+
174+ Args:
175+ template_path: The path to store
176+
177+ """
178+ ProgramSettings .store_template_dir (template_path )
179+
180+ def _on_row_double_click (self , event : tk .Event ) -> None :
164181 """Handle row double-click event."""
165182 item_id = self .tree .identify_row (event .y )
166183 if item_id :
167184 selected_template_relative_path = self .tree .item (item_id )["text" ]
168- ProgramSettings .store_template_dir (selected_template_relative_path )
169- self .root .destroy ()
185+ self .store_template_dir (selected_template_relative_path )
186+ self .close_window ()
187+
188+ def close_window (self ) -> None :
189+ """Close the window - separated for testability."""
190+ self .root .destroy ()
170191
171- def __sort_by_column (self , col : str , reverse : bool ) -> None :
192+ def _sort_by_column (self , col : str , reverse : bool ) -> None :
193+ """Sort treeview items by the specified column."""
172194 if hasattr (self , "sort_column" ) and self .sort_column and self .sort_column != col :
173195 self .tree .heading (self .sort_column , text = self .sort_column )
174196 self .tree .heading (col , text = col + (" ▼" if reverse else " ▲" ))
@@ -185,7 +207,7 @@ def __sort_by_column(self, col: str, reverse: bool) -> None:
185207 self .tree .move (k , "" , index )
186208
187209 # reverse sort next time
188- self .tree .heading (col , command = lambda : self .__sort_by_column (col , not reverse ))
210+ self .tree .heading (col , command = lambda : self ._sort_by_column (col , not reverse ))
189211
190212 def _display_vehicle_image (self , template_path : str ) -> None :
191213 """Display the vehicle image corresponding to the selected template."""
@@ -194,7 +216,7 @@ def _display_vehicle_image(self, template_path: str) -> None:
194216 if isinstance (widget , ttk .Label ) and widget == self .image_label :
195217 widget .destroy ()
196218 try :
197- vehicle_image_filepath = VehicleComponents .get_vehicle_image_filepath (template_path )
219+ vehicle_image_filepath = self .get_vehicle_image_filepath (template_path )
198220 self .image_label = self .put_image_in_label (self .top_frame , vehicle_image_filepath , IMAGE_HEIGHT_PX )
199221 except FileNotFoundError :
200222 self .image_label = ttk .Label (
@@ -204,6 +226,36 @@ def _display_vehicle_image(self, template_path: str) -> None:
204226 )
205227 self .image_label .pack (side = tk .RIGHT , anchor = tk .NE , padx = (4 , 0 ), pady = (0 , 0 ))
206228
229+ def get_vehicle_image_filepath (self , template_path : str ) -> str :
230+ """
231+ Get the filepath for a vehicle image.
232+
233+ Separated from display method for testability.
234+
235+ Args:
236+ template_path: Path to the template
237+
238+ Returns:
239+ Path to the vehicle image
240+
241+ Raises:
242+ FileNotFoundError: If the image file doesn't exist
243+
244+ """
245+ return VehicleComponents .get_vehicle_image_filepath (template_path )
246+
247+ def run_app (self ) -> None :
248+ """Run the TemplateOverviewWindow application."""
249+ if isinstance (self .root , tk .Toplevel ):
250+ try :
251+ while self .root .children :
252+ self .root .update_idletasks ()
253+ self .root .update ()
254+ except tk .TclError as _exp :
255+ pass
256+ elif isinstance (self .root , tk .Tk ):
257+ self .root .mainloop ()
258+
207259
208260def argument_parser () -> argparse .Namespace :
209261 """
@@ -217,25 +269,38 @@ def argument_parser() -> argparse.Namespace:
217269 """
218270 parser = argparse .ArgumentParser (
219271 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 ."
226278 )
227279 )
228280 return add_common_arguments (parser ).parse_args ()
229281
230282
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+
231294def main () -> None :
295+ """Main entry point for the application."""
232296 args = argument_parser ()
297+ setup_logging (args .loglevel )
233298
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 ()
237301
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 ])
239304
240305
241306if __name__ == "__main__" :
0 commit comments