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,24 @@ 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 ()
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 ()
6788
89+ def _setup_treeview (self ) -> None :
90+ """Set up the treeview with columns and styling."""
6891 style = ttk .Style (self .root )
6992 # Add padding to Treeview heading style
7093 style .layout (
@@ -99,76 +122,87 @@ def __init__(self, parent: Optional[tk.Tk] = None) -> None:
99122 for col in columns :
100123 self .tree .heading (col , text = col )
101124
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."""
103131 for key , template_overview in VehicleComponents .get_vehicle_components_overviews ().items ():
104132 attribute_names = template_overview .attributes ()
105133 values = (key , * (getattr (template_overview , attr , "" ) for attr in attribute_names ))
106134 self .tree .insert ("" , "end" , text = key , values = values )
107135
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-
134136 def _adjust_treeview_column_widths (self ) -> None :
135137 """Adjusts the column widths of the Treeview to fit the contents of each column."""
136138 for col in self .tree ["columns" ]:
137139 max_width = 0
138140 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 ))
140142
141143 # Iterate over all rows and update the max_width if a wider entry is found
142144 for item in self .tree .get_children ():
143145 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 )
145147 max_width = max (max_width , text_width )
146148
147149 # Update the column's width property to accommodate the largest text width
148150 self .tree .column (col , width = int (max_width * 0.6 + 10 ))
149151
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 :
151168 """Handle row single-click event."""
152- self .root .after (0 , self .__update_selection )
169+ self .root .after (0 , self ._update_selection )
153170
154- def __update_selection (self ) -> None :
171+ def _update_selection (self ) -> None :
155172 """Update selection after keypress event."""
156173 selected_item = self .tree .selection ()
157174 if selected_item :
158175 item_id = selected_item [0 ]
159176 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 )
161178 self ._display_vehicle_image (selected_template_relative_path )
162179
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 :
164193 """Handle row double-click event."""
165194 item_id = self .tree .identify_row (event .y )
166195 if item_id :
167196 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 ()
170203
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."""
172206 if hasattr (self , "sort_column" ) and self .sort_column and self .sort_column != col :
173207 self .tree .heading (self .sort_column , text = self .sort_column )
174208 self .tree .heading (col , text = col + (" ▼" if reverse else " ▲" ))
@@ -185,7 +219,7 @@ def __sort_by_column(self, col: str, reverse: bool) -> None:
185219 self .tree .move (k , "" , index )
186220
187221 # 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 ))
189223
190224 def _display_vehicle_image (self , template_path : str ) -> None :
191225 """Display the vehicle image corresponding to the selected template."""
@@ -194,7 +228,7 @@ def _display_vehicle_image(self, template_path: str) -> None:
194228 if isinstance (widget , ttk .Label ) and widget == self .image_label :
195229 widget .destroy ()
196230 try :
197- vehicle_image_filepath = VehicleComponents .get_vehicle_image_filepath (template_path )
231+ vehicle_image_filepath = self .get_vehicle_image_filepath (template_path )
198232 self .image_label = self .put_image_in_label (self .top_frame , vehicle_image_filepath , IMAGE_HEIGHT_PX )
199233 except FileNotFoundError :
200234 self .image_label = ttk .Label (
@@ -204,6 +238,24 @@ def _display_vehicle_image(self, template_path: str) -> None:
204238 )
205239 self .image_label .pack (side = tk .RIGHT , anchor = tk .NE , padx = (4 , 0 ), pady = (0 , 0 ))
206240
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+
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