From 6aa84b3d89fadc431727bd28997e0c2a8bded90c Mon Sep 17 00:00:00 2001 From: "Dr.-Ing. Amilcar do Carmo Lucas" Date: Thu, 6 Jun 2024 20:33:22 +0200 Subject: [PATCH] IMPROVEMENT: Use the more modern ttk widgets --- MethodicConfigurator/frontend_tkinter_base.py | 38 ++++++----- .../frontend_tkinter_component_editor_base.py | 16 +++-- .../frontend_tkinter_connection_selection.py | 59 +++++++++------- .../frontend_tkinter_directory_selection.py | 47 +++++++------ .../frontend_tkinter_flightcontroller_info.py | 7 +- .../frontend_tkinter_parameter_editor.py | 67 ++++++++++--------- ...frontend_tkinter_parameter_editor_table.py | 35 +++++----- 7 files changed, 147 insertions(+), 122 deletions(-) diff --git a/MethodicConfigurator/frontend_tkinter_base.py b/MethodicConfigurator/frontend_tkinter_base.py index 1ec3df2..289608c 100644 --- a/MethodicConfigurator/frontend_tkinter_base.py +++ b/MethodicConfigurator/frontend_tkinter_base.py @@ -8,6 +8,8 @@ SPDX-License-Identifier: GPL-3 ''' +# https://wiki.tcl-lang.org/page/Changing+Widget+Colors + import tkinter as tk from tkinter import messagebox from tkinter import ttk @@ -60,7 +62,7 @@ def leave(_event): tooltip = tk.Toplevel(widget) tooltip.wm_overrideredirect(True) - tooltip_label = tk.Label(tooltip, text=text, bg="#ffffe0", relief="solid", borderwidth=1, justify=tk.LEFT) + tooltip_label = ttk.Label(tooltip, text=text, background="#ffffe0", relief="solid", borderwidth=1, justify=tk.LEFT) tooltip_label.pack() tooltip.withdraw() # Initially hide the tooltip @@ -112,23 +114,25 @@ def set_entries_tupple(self, values, selected_element, tooltip=None): show_tooltip(self, tooltip) -class ScrollFrame(tk.Frame): +class ScrollFrame(ttk.Frame): """ A custom Frame widget that supports scrolling. - This class extends the tk.Frame widget to include a canvas and a scrollbar, + This class extends the ttk.Frame widget to include a canvas and a scrollbar, allowing for scrolling content within the frame. It's useful for creating scrollable areas within your application's GUI. """ def __init__(self, parent): super().__init__(parent) # create a frame (self) - self.canvas = tk.Canvas(self, borderwidth=0) # place canvas on self + # place canvas on self, copy ttk.background to tk.background + self.canvas = tk.Canvas(self, borderwidth=0, background=ttk.Style(parent).lookup('TFrame', 'background')) # place a frame on the canvas, this frame will hold the child widgets - self.view_port = tk.Frame(self.canvas) + self.view_port = ttk.Frame(self.canvas) - self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview) # place a scrollbar on self + # place a tk.scrollbar on self. ttk.scrollbar will not work here + self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview) # attach scrollbar action to scroll of canvas self.canvas.configure(yscrollcommand=self.vsb.set) @@ -218,12 +222,15 @@ def __create_progress_window(self, title: str, message, width, height): self.progress_window.title(title) self.progress_window.geometry(f"{width}x{height}") + main_frame = ttk.Frame(self.progress_window) + main_frame.pack(expand=True, fill=tk.BOTH) + # Create a progress bar - self.progress_bar = ttk.Progressbar(self.progress_window, length=100, mode='determinate') + self.progress_bar = ttk.Progressbar(main_frame, length=100, mode='determinate') self.progress_bar.pack(side=tk.TOP, fill=tk.X, expand=False, padx=(5, 5), pady=(10, 10)) # Create a label to display the progress message - self.progress_label = tk.Label(self.progress_window, text=message.format(0, 0)) + self.progress_label = ttk.Label(main_frame, text=message.format(0, 0)) self.progress_label.pack(side=tk.TOP, fill=tk.X, expand=False, pady=(10, 10)) self.progress_window.lift() @@ -280,18 +287,15 @@ def __init__(self, root_tk: tk.Tk=None): # Set the theme to 'alt' style = ttk.Style() style.theme_use('alt') + style.configure("Bold.TLabel", font=("TkDefaultFont", 11, "bold")) + + self.main_frame = ttk.Frame(self.root) + self.main_frame.pack(expand=True, fill=tk.BOTH) # Set the application icon for the window and all child windows # https://pythonassets.com/posts/window-icon-in-tk-tkinter/ self.root.iconphoto(True, tk.PhotoImage(file=LocalFilesystem.application_icon_filepath())) - # Get the background color for the 'TFrame' widget - self.default_background_color = '#f0f0f0' # style.lookup('TFrame', 'background') - - # Configure the background color for the checkbutton - style.configure('TCheckbutton', background=self.default_background_color) - style.configure('TCombobox', background=self.default_background_color) - @staticmethod def center_window(window, parent): """ @@ -311,7 +315,7 @@ def center_window(window, parent): window.geometry(f"+{x}+{y}") @staticmethod - def put_image_in_label(parent: tk.Toplevel, filepath: str, image_height: int=40) -> tk.Label: + def put_image_in_label(parent: tk.Toplevel, filepath: str, image_height: int=40) -> ttk.Label: # Load the image and scale it down to image_height pixels in height image = Image.open(filepath) width, height = image.size @@ -323,6 +327,6 @@ def put_image_in_label(parent: tk.Toplevel, filepath: str, image_height: int=40) photo = ImageTk.PhotoImage(resized_image) # Create a label with the resized image - image_label = tk.Label(parent, image=photo) + image_label = ttk.Label(parent, image=photo) image_label.image = photo # Keep a reference to the image to prevent it from being garbage collected return image_label diff --git a/MethodicConfigurator/frontend_tkinter_component_editor_base.py b/MethodicConfigurator/frontend_tkinter_component_editor_base.py index 982faba..4b17b3d 100644 --- a/MethodicConfigurator/frontend_tkinter_component_editor_base.py +++ b/MethodicConfigurator/frontend_tkinter_component_editor_base.py @@ -69,32 +69,34 @@ def __init__(self, version, local_filesystem: LocalFilesystem=None): self.entry_widgets = {} # Dictionary for entry widgets - main_frame = ttk.Frame(self.root) - main_frame.pack(side=tk.TOP, fill="x", expand=False, pady=(4, 0)) # Pack the frame at the top of the window + intro_frame = ttk.Frame(self.main_frame) + intro_frame.pack(side=tk.TOP, fill="x", expand=False) explanation_text = "Please configure ALL vehicle component properties in this window.\n" explanation_text += "Scroll down and make sure you do not miss a property.\n" explanation_text += "Saving the result will write to the vehicle_components.json file." - explanation_label = tk.Label(main_frame, text=explanation_text, wraplength=800, justify=tk.LEFT) + explanation_label = ttk.Label(intro_frame, text=explanation_text, wraplength=800, justify=tk.LEFT) explanation_label.pack(side=tk.LEFT, padx=(10, 10), pady=(10, 0), anchor=tk.NW) # Load the vehicle image and scale it down to image_height pixels in height if local_filesystem.vehicle_image_exists(): - image_label = self.put_image_in_label(main_frame, local_filesystem.vehicle_image_filepath(), 100) + image_label = self.put_image_in_label(intro_frame, local_filesystem.vehicle_image_filepath(), 100) image_label.pack(side=tk.RIGHT, anchor=tk.NE, padx=(4, 4), pady=(4, 0)) show_tooltip(image_label, "Replace the vehicle.jpg file in the vehicle directory to change the vehicle image.") else: - image_label = tk.Label(main_frame, text="No vehicle.jpg image file found on the vehicle directory.") + image_label = ttk.Label(intro_frame, text="No vehicle.jpg image file found on the vehicle directory.") image_label.pack(side=tk.RIGHT, anchor=tk.NE, padx=(4, 4), pady=(4, 0)) - self.scroll_frame = ScrollFrame(self.root) + self.scroll_frame = ScrollFrame(self.main_frame) self.scroll_frame.pack(side="top", fill="both", expand=True) self.update_json_data() self.__populate_frames() - self.save_button = ttk.Button(self.root, text="Save data and start configuration", command=self.save_data) + save_frame = ttk.Frame(self.main_frame) + save_frame.pack(side=tk.TOP, fill="x", expand=False) + self.save_button = ttk.Button(save_frame, text="Save data and start configuration", command=self.save_data) show_tooltip(self.save_button, "Save component data and start parameter value configuration and tuning.") self.save_button.pack(pady=7) diff --git a/MethodicConfigurator/frontend_tkinter_connection_selection.py b/MethodicConfigurator/frontend_tkinter_connection_selection.py index c1a9da2..a665450 100644 --- a/MethodicConfigurator/frontend_tkinter_connection_selection.py +++ b/MethodicConfigurator/frontend_tkinter_connection_selection.py @@ -99,11 +99,11 @@ def __init__(self, parent, parent_frame, flight_controller: FlightController, # self.connection_progress_window = None # Create a new frame for the flight controller connection selection label and combobox - self.container_frame = tk.Frame(parent_frame) + self.container_frame = ttk.Frame(parent_frame) # Create a description label for the flight controller connection selection - conn_selection_label = tk.Label(self.container_frame, text="flight controller connection:") - conn_selection_label.pack(side=tk.TOP, anchor=tk.NW) # Add the label to the top of the conn_selection_frame + conn_selection_label = ttk.Label(self.container_frame, text="flight controller connection:") + conn_selection_label.pack(side=tk.TOP) # Add the label to the top of the conn_selection_frame # Create a read-only combobox for flight controller connection selection self.conn_selection_combobox = PairTupleCombobox(self.container_frame, self.flight_controller.get_connection_tuples(), @@ -111,7 +111,7 @@ def __init__(self, parent, parent_frame, flight_controller: FlightController, # "FC connection", state='readonly') self.conn_selection_combobox.bind("<>", self.on_select_connection_combobox_change) - self.conn_selection_combobox.pack(side=tk.TOP, anchor=tk.NW, pady=(4, 0)) + self.conn_selection_combobox.pack(side=tk.TOP, pady=(4, 0)) show_tooltip(self.conn_selection_combobox, "Select the flight controller connection\nYou can add a custom connection " "to the existing ones") @@ -174,7 +174,7 @@ class ConnectionSelectionWindow(BaseWindow): def __init__(self, flight_controller: FlightController, connection_result_string: str): super().__init__() self.root.title("Flight controller connection") - self.root.geometry("420x510") # Set the window size + self.root.geometry("460x450") # Set the window size # Explain why we are here if flight_controller.comport is None: @@ -184,45 +184,56 @@ def __init__(self, flight_controller: FlightController, connection_result_string introduction_text = connection_result_string.replace(":", ":\n") else: introduction_text = connection_result_string - self.introduction_label = tk.Label(self.root, text=introduction_text + "\nChoose one of the following three options:") + self.introduction_label = ttk.Label(self.main_frame, anchor=tk.CENTER, justify=tk.CENTER, + text=introduction_text + "\nChoose one of the following three options:") self.introduction_label.pack(expand=False, fill=tk.X, padx=6, pady=6) # Option 1 - Auto-connect - option1_label_frame = tk.LabelFrame(self.root, text="Auto-connect to flight controller") + option1_label = ttk.Label(text="Auto-connect to flight controller", style="Bold.TLabel") + option1_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option1_label, borderwidth=2, relief="solid") option1_label_frame.pack(expand=False, fill=tk.X, padx=6, pady=6) - option1_label = tk.Label(option1_label_frame, text="Connect a flight controller to the PC,\n" - "wait 7 seconds for it to fully boot and\n" - "press the Auto-connect button below to connect to it") + option1_label = ttk.Label(option1_label_frame, anchor=tk.CENTER, justify=tk.CENTER, + text="Connect a flight controller to the PC,\n" + "wait 7 seconds for it to fully boot and\n" + "press the Auto-connect button below to connect to it") option1_label.pack(expand=False, fill=tk.X, padx=6) - autoconnect_button = tk.Button(option1_label_frame, text="Auto-connect", command=self.fc_autoconnect) + autoconnect_button = ttk.Button(option1_label_frame, text="Auto-connect", command=self.fc_autoconnect) autoconnect_button.pack(expand=False, fill=tk.X, padx=100, pady=6) + show_tooltip(autoconnect_button, "Auto-connect to a 'Mavlink'-talking serial device") # Option 2 - Manually select the flight controller connection or add a new one - option2_label_frame = tk.LabelFrame(self.root, text="Select flight controller connection") + option2_label = ttk.Label(text="Select flight controller connection", style="Bold.TLabel") + option2_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option2_label, borderwidth=2, relief="solid") option2_label_frame.pack(expand=False, fill=tk.X, padx=6, pady=6) - option2_label = tk.Label(option2_label_frame, text="Connect a flight controller to the PC,\n" - "wait 7 seconds for it to fully boot and\n" - "manually select the fight controller connection or add a new one") + option2_label = ttk.Label(option2_label_frame, anchor=tk.CENTER, justify=tk.CENTER, + text="Connect a flight controller to the PC,\n" + "wait 7 seconds for it to fully boot and\n" + "manually select the fight controller connection or add a new one") option2_label.pack(expand=False, fill=tk.X, padx=6) self.connection_selection_widgets = ConnectionSelectionWidgets(self, option2_label_frame, flight_controller, destroy_parent_on_connect=True, download_params_on_connect=False) - self.connection_selection_widgets.container_frame.pack(expand=True, fill=tk.X, padx=80, pady=6, anchor=tk.CENTER) + self.connection_selection_widgets.container_frame.pack(expand=False, fill=tk.X, padx=80, pady=6) # Option 3 - Skip FC connection, just edit the .param files on disk - option3_label_frame = tk.LabelFrame(self.root, text="No flight controller connection") + option3_label = ttk.Label(text="No flight controller connection", style="Bold.TLabel") + option3_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option3_label, borderwidth=2, relief="solid") option3_label_frame.pack(expand=False, fill=tk.X, padx=6, pady=6) - option3_label = tk.Label(option3_label_frame, text="Skip the flight controller connection,\n" - "no default parameter values will be fetched from the FC,\n" - "default parameter values from disk will be used instead\n" - "(if '00_default.param' file is present)\n" - "and just edit the intermediate '.param' files on disk") - option3_label.pack(expand=False, fill=tk.X, padx=6) - skip_fc_connection_button = tk.Button(option3_label_frame, + #option3_label = ttk.Label(option3_label_frame, anchor=tk.CENTER, justify=tk.CENTER, + # text="Skip the flight controller connection,\n" + # "no default parameter values will be fetched from the FC,\n" + # "default parameter values from disk will be used instead\n" + # "(if '00_default.param' file is present)\n" + # "and just edit the intermediate '.param' files on disk") + #option3_label.pack(expand=False, fill=tk.X, padx=6) + skip_fc_connection_button = ttk.Button(option3_label_frame, text="Skip FC connection, just edit the .param files on disk", command=lambda flight_controller=flight_controller: self.skip_fc_connection(flight_controller)) skip_fc_connection_button.pack(expand=False, fill=tk.X, padx=15, pady=6) + show_tooltip(skip_fc_connection_button, + "No parameter values will be fetched from the FC, default parameter values from disk will be used\n" + "instead (if '00_default.param' file is present) and just edit the intermediate '.param' files on disk") # Bind the close_connection_and_quit function to the window close event self.root.protocol("WM_DELETE_WINDOW", self.close_and_quit) diff --git a/MethodicConfigurator/frontend_tkinter_directory_selection.py b/MethodicConfigurator/frontend_tkinter_directory_selection.py index fd90d1a..b049ec2 100644 --- a/MethodicConfigurator/frontend_tkinter_directory_selection.py +++ b/MethodicConfigurator/frontend_tkinter_directory_selection.py @@ -24,7 +24,6 @@ from tkinter import messagebox from tkinter import ttk from tkinter import filedialog -from tkinter import Checkbutton from MethodicConfigurator.version import VERSION @@ -53,15 +52,15 @@ def __init__(self, parent, parent_frame, initialdir: str, label_text: str, # py self.autoresize_width = autoresize_width # Create a new frame for the directory selection label and button - self.container_frame = tk.Frame(parent_frame) + self.container_frame = ttk.Frame(parent_frame) # Create a description label for the directory - directory_selection_label = tk.Label(self.container_frame, text=label_text) + directory_selection_label = ttk.Label(self.container_frame, text=label_text) directory_selection_label.pack(side=tk.TOP, anchor=tk.NW) show_tooltip(directory_selection_label, dir_tooltip) # Create a new subframe for the directory selection - directory_selection_subframe = tk.Frame(self.container_frame) + directory_selection_subframe = ttk.Frame(self.container_frame) directory_selection_subframe.pack(side=tk.TOP, fill="x", expand=False, anchor=tk.NW) # Create a read-only entry for the directory @@ -111,16 +110,16 @@ class DirectoryNameWidgets(): # pylint: disable=too-few-public-methods """ def __init__(self, parent_frame, initial_dir: str, label_text: str, dir_tooltip: str): # Create a new frame for the directory name selection label - self.container_frame = tk.Frame(parent_frame) + self.container_frame = ttk.Frame(parent_frame) # Create a description label for the directory name entry - directory_selection_label = tk.Label(self.container_frame, text=label_text) + directory_selection_label = ttk.Label(self.container_frame, text=label_text) directory_selection_label.pack(side=tk.TOP, anchor=tk.NW) show_tooltip(directory_selection_label, dir_tooltip) # Create an entry for the directory self.dir_var = tk.StringVar(value=initial_dir) - directory_entry = tk.Entry(self.container_frame, textvariable=self.dir_var, + directory_entry = ttk.Entry(self.container_frame, textvariable=self.dir_var, width=max(4, len(initial_dir))) directory_entry.pack(side=tk.LEFT, fill="x", expand=True, anchor=tk.NW, pady=(4, 0)) show_tooltip(directory_entry, dir_tooltip) @@ -136,7 +135,7 @@ class VehicleDirectorySelectionWidgets(DirectorySelectionWidgets): directory selections. It includes additional logic for updating the local filesystem with the selected vehicle directory and re-initializing the filesystem with the new directory. """ - def __init__(self, parent: tk, parent_frame: tk.Frame, # pylint: disable=too-many-arguments + def __init__(self, parent: ttk, parent_frame: ttk.Frame, # pylint: disable=too-many-arguments local_filesystem: LocalFilesystem, initial_dir: str, destroy_parent_on_open: bool) -> None: # Call the parent constructor with the necessary arguments @@ -205,8 +204,8 @@ def __init__(self, local_filesystem: LocalFilesystem, fc_connected: bool = False introduction_text = "No intermediate parameter files found\nin current working directory." else: introduction_text = "No intermediate parameter files found\nin the --vehicle-dir specified directory." - introduction_label = tk.Label(self.root, text=introduction_text + \ - "\nChoose one of the following three options:") + introduction_label = ttk.Label(self.main_frame, anchor=tk.CENTER, justify=tk.CENTER, + text=introduction_text + "\nChoose one of the following three options:") introduction_label.pack(expand=False, fill=tk.X, padx=6, pady=6) template_dir, new_base_dir, vehicle_dir = LocalFilesystem.get_recently_used_dirs() self.create_option1_widgets(template_dir, @@ -222,11 +221,11 @@ def __init__(self, local_filesystem: LocalFilesystem, fc_connected: bool = False def close_and_quit(self): sys_exit(0) - def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: str, initial_new_dir: str, - fc_connected: bool): + def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: str, + initial_new_dir: str, fc_connected: bool): # Option 1 - Create a new vehicle configuration directory based on an existing template - option1_label_frame = tk.LabelFrame(self.root, text="Create a new vehicle configuration directory", - font= ('Helvetica 11 bold'), borderwidth=2, relief="solid") + option1_label = ttk.Label(text="Create a new vehicle configuration directory", style="Bold.TLabel") + option1_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option1_label, borderwidth=2, relief="solid") option1_label_frame.pack(expand=True, fill=tk.X, padx=6, pady=6) template_dir_edit_tooltip = "Existing vehicle template directory containing the intermediate\n" \ "parameter files to be copied to the new vehicle configuration directory" @@ -239,8 +238,8 @@ def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: st template_dir_btn_tooltip) self.template_dir.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=5, anchor=tk.NW) - use_fc_params_checkbox = Checkbutton(option1_label_frame, variable=self.use_fc_params, - text="Use parameter values from connected FC, not from template files") + use_fc_params_checkbox = ttk.Checkbutton(option1_label_frame, variable=self.use_fc_params, + text="Use parameter values from connected FC, not from template files") use_fc_params_checkbox.pack(anchor=tk.NW) show_tooltip(use_fc_params_checkbox, "Use the parameter values from the connected flight controller instead of the\n" \ @@ -264,7 +263,7 @@ def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: st "(destination) new vehicle name:", new_dir_edit_tooltip) self.new_dir.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=5, anchor=tk.NW) - create_vehicle_directory_from_template_button = tk.Button(option1_label_frame, + create_vehicle_directory_from_template_button = ttk.Button(option1_label_frame, text="Create vehicle configuration directory from template", command=self.create_new_vehicle_from_template) create_vehicle_directory_from_template_button.pack(expand=False, fill=tk.X, padx=20, pady=5, anchor=tk.CENTER) @@ -275,11 +274,11 @@ def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: st def create_option2_widgets(self, initial_dir: str): # Option 2 - Use an existing vehicle configuration directory - option2_label_frame = tk.LabelFrame(self.root, text="Open an existing vehicle configuration directory", - font= ('Helvetica 11 bold'), borderwidth=2, relief="solid") + option2_label = ttk.Label(text="Open an existing vehicle configuration directory", style="Bold.TLabel") + option2_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option2_label, borderwidth=2, relief="solid") option2_label_frame.pack(expand=True, fill=tk.X, padx=6, pady=6) - option2_label = tk.Label(option2_label_frame, - text="Use an existing vehicle configuration directory with\n" \ + option2_label = ttk.Label(option2_label_frame, anchor=tk.CENTER, justify=tk.CENTER, + text="Use an existing vehicle configuration directory with\n" \ "intermediate parameter files, apm.pdef.xml and vehicle_components.json") option2_label.pack(expand=False, fill=tk.X, padx=6) self.connection_selection_widgets = VehicleDirectorySelectionWidgets(self, option2_label_frame, @@ -290,8 +289,8 @@ def create_option2_widgets(self, initial_dir: str): def create_option3_widgets(self, last_vehicle_dir: str): # Option 3 - Open the last used vehicle configuration directory - option3_label_frame = tk.LabelFrame(self.root, text="Open the last used vehicle configuration directory", - font= ('Helvetica 11 bold'), borderwidth=2, relief="solid") + option3_label = ttk.Label(text="Open the last used vehicle configuration directory", style="Bold.TLabel") + option3_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option3_label, borderwidth=2, relief="solid") option3_label_frame.pack(expand=True, fill=tk.X, padx=6, pady=6) last_dir = DirectorySelectionWidgets(self, option3_label_frame, last_vehicle_dir if last_vehicle_dir else '', @@ -303,7 +302,7 @@ def create_option3_widgets(self, last_vehicle_dir: str): # Check if there is a last used vehicle configuration directory button_state = tk.NORMAL if last_vehicle_dir else tk.DISABLED - open_last_vehicle_directory_button = tk.Button(option3_label_frame, + open_last_vehicle_directory_button = ttk.Button(option3_label_frame, text="Open Last Used Vehicle Configuration Directory", command=lambda last_vehicle_dir=last_vehicle_dir: \ self.open_last_vehicle_directory(last_vehicle_dir), diff --git a/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py b/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py index 6ede26b..4b813f8 100644 --- a/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py +++ b/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py @@ -12,6 +12,7 @@ from logging import info as logging_info import tkinter as tk +from tkinter import ttk from MethodicConfigurator.backend_flightcontroller import FlightController #from MethodicConfigurator.backend_flightcontroller_info import BackendFlightcontrollerInfo @@ -34,7 +35,7 @@ def __init__(self, flight_controller: FlightController): self.flight_controller = flight_controller # Create a frame to hold all the labels and text fields - self.info_frame = tk.Frame(self.root) + self.info_frame = ttk.Frame(self.main_frame) self.info_frame.pack(padx=20, pady=20) # Dictionary mapping attribute names to their descriptions @@ -55,10 +56,10 @@ def __init__(self, flight_controller: FlightController): # Dynamically create labels and text fields for each attribute for row_nr, (description, attr_name) in enumerate(attribute_descriptions.items()): - label = tk.Label(self.info_frame, text=f"{description}:") + label = ttk.Label(self.info_frame, text=f"{description}:") label.grid(row=row_nr, column=0, sticky="w") - text_field = tk.Entry(self.info_frame, width=60) + text_field = ttk.Entry(self.info_frame, width=60) text_field.grid(row=row_nr, column=1, sticky="w") # Check if the attribute exists and has a non-empty value before inserting diff --git a/MethodicConfigurator/frontend_tkinter_parameter_editor.py b/MethodicConfigurator/frontend_tkinter_parameter_editor.py index 913e124..ef4d9df 100644 --- a/MethodicConfigurator/frontend_tkinter_parameter_editor.py +++ b/MethodicConfigurator/frontend_tkinter_parameter_editor.py @@ -59,11 +59,11 @@ def __init__(self, root: tk.Tk, local_filesystem, current_file: str): self.__create_documentation_frame() def __create_documentation_frame(self): - self.documentation_frame = tk.LabelFrame(self.root, text="Documentation") + self.documentation_frame = ttk.LabelFrame(self.root, text="Documentation") self.documentation_frame.pack(side=tk.TOP, fill="x", expand=False, pady=(4, 4), padx=(4, 4)) # Create a grid structure within the documentation_frame - documentation_grid = tk.Frame(self.documentation_frame) + documentation_grid = ttk.Frame(self.documentation_frame) documentation_grid.pack(fill="both", expand=True) descriptive_texts = ["Forum Blog:", "Wiki:", "External tool:", "Mandatory:"] @@ -74,12 +74,12 @@ def __create_documentation_frame(self): "vehicle,\n 0% you can ignore this file if it does not apply to your vehicle"] for i, text in enumerate(descriptive_texts): # Create labels for the first column with static descriptive text - label = tk.Label(documentation_grid, text=text) + label = ttk.Label(documentation_grid, text=text) label.grid(row=i, column=0, sticky="w") show_tooltip(label, descriptive_tooltips[i]) # Create labels for the second column with the documentation links - self.documentation_labels[text] = tk.Label(documentation_grid) + self.documentation_labels[text] = ttk.Label(documentation_grid) self.documentation_labels[text].grid(row=i, column=1, sticky="w") # Dynamically update the documentation text and URL links @@ -107,11 +107,11 @@ def update_documentation_labels(self, current_file: str): def __update_documentation_label(self, label_key, text, url, url_expected=True): label = self.documentation_labels[label_key] if url: - label.config(text=text, fg="blue", cursor="hand2", underline=True) + label.config(text=text, foreground="blue", cursor="hand2", underline=True) label.bind("", lambda event, url=url: webbrowser_open(url)) show_tooltip(label, url) else: - label.config(text=text, fg="black", cursor="arrow", underline=False) + label.config(text=text, foreground="black", cursor="arrow", underline=False) label.bind("", lambda event: None) if url_expected: show_tooltip(label, "Documentation URL not available") @@ -123,29 +123,32 @@ def show_about_window(root, version: str): about_window.title("About") about_window.geometry("650x220") + main_frame = ttk.Frame(about_window) + main_frame.pack(expand=True, fill=tk.BOTH) + # Add the "About" message about_message = f"ArduPilot Methodic Configurator Version: {version}\n\n" \ "A clear configuration sequence for ArduPilot vehicles.\n\n" \ "Copyright © 2024 Amilcar do Carmo Lucas and ArduPilot.org\n\n" \ "Licensed under the GNU General Public License v3.0" - about_label = tk.Label(about_window, text=about_message, wraplength=450) + about_label = ttk.Label(main_frame, text=about_message, wraplength=450) about_label.pack(padx=10, pady=10) # Create buttons for each action - user_manual_button = tk.Button(about_window, text="User Manual", - command=lambda: webbrowser_open( + user_manual_button = ttk.Button(main_frame, text="User Manual", + command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator/blob/master/USERMANUAL.md")) - support_forum_button = tk.Button(about_window, text="Support Forum", - command=lambda: webbrowser_open( + support_forum_button = ttk.Button(main_frame, text="Support Forum", + command=lambda: webbrowser_open( "http://discuss.ardupilot.org/t/new-ardupilot-methodic-configurator-gui/115038/1")) - report_bug_button = tk.Button(about_window, text="Report a Bug", - command=lambda: webbrowser_open( + report_bug_button = ttk.Button(main_frame, text="Report a Bug", + command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator/issues/new")) - credits_button = tk.Button(about_window, text="Credits", - command=lambda: webbrowser_open( + credits_button = ttk.Button(main_frame, text="Credits", + command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator/blob/master/credits/CREDITS.md")) - source_button = tk.Button(about_window, text="Source Code", - command=lambda: webbrowser_open( + source_button = ttk.Button(main_frame, text="Source Code", + command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator")) # Pack the buttons @@ -189,7 +192,7 @@ def __init__(self, current_file: str, flight_controller: FlightController, self.__create_conf_widgets(version) # Create a DocumentationFrame object for the Documentation Content - self.documentation_frame = DocumentationFrame(self.root, self.local_filesystem, self.current_file) + self.documentation_frame = DocumentationFrame(self.main_frame, self.local_filesystem, self.current_file) self.__create_parameter_area_widgets() @@ -197,10 +200,10 @@ def __init__(self, current_file: str, flight_controller: FlightController, self.root.mainloop() def __create_conf_widgets(self, version: str): - config_frame = tk.Frame(self.root) + config_frame = ttk.Frame(self.main_frame) config_frame.pack(side=tk.TOP, fill="x", expand=False, pady=(4, 0)) # Pack the frame at the top of the window - config_subframe = tk.Frame(config_frame) + config_subframe = ttk.Frame(config_frame) config_subframe.pack(side=tk.LEFT, fill="x", expand=True, anchor=tk.NW) # Pack the frame at the top of the window # Create a new frame inside the config_subframe for the intermediate parameter file directory selection labels @@ -211,11 +214,11 @@ def __create_conf_widgets(self, version: str): directory_selection_frame.container_frame.pack(side=tk.LEFT, fill="x", expand=False, padx=(4, 6)) # Create a new frame inside the config_subframe for the intermediate parameter file selection label and combobox - file_selection_frame = tk.Frame(config_subframe) + file_selection_frame = ttk.Frame(config_subframe) file_selection_frame.pack(side=tk.LEFT, fill="x", expand=False, padx=(6, 6)) # Create a label for the Combobox - file_selection_label = tk.Label(file_selection_frame, text="Current intermediate parameter file:") + file_selection_label = ttk.Label(file_selection_frame, text="Current intermediate parameter file:") file_selection_label.pack(side=tk.TOP, anchor=tk.NW) # Add the label to the top of the file_selection_frame # Create Combobox for intermediate parameter file selection @@ -232,7 +235,7 @@ def __create_conf_widgets(self, version: str): image_label = BaseWindow.put_image_in_label(config_frame, LocalFilesystem.application_logo_filepath()) image_label.pack(side=tk.RIGHT, anchor=tk.NE, padx=(4, 4), pady=(4, 0)) - image_label.bind("", lambda event: show_about_window(self.root, version)) + image_label.bind("", lambda event: show_about_window(self.main_frame, version)) show_tooltip(image_label, "User Manual, Support Forum, Report a Bug, Credits, Source Code") def __create_parameter_area_widgets(self): @@ -240,16 +243,16 @@ def __create_parameter_area_widgets(self): self.annotate_params_into_files = tk.BooleanVar(value=False) # Create a Scrollable parameter editor table - self.parameter_editor_table = ParameterEditorTable(self.root, self.local_filesystem, self) + self.parameter_editor_table = ParameterEditorTable(self.main_frame, self.local_filesystem, self) self.repopulate_parameter_table(self.current_file) self.parameter_editor_table.pack(side="top", fill="both", expand=True) # Create a frame for the buttons - buttons_frame = tk.Frame(self.root) + buttons_frame = ttk.Frame(self.main_frame) buttons_frame.pack(side="bottom", fill="x", expand=False, pady=(10, 10)) # Create a frame for the checkboxes - checkboxes_frame = tk.Frame(buttons_frame) + checkboxes_frame = ttk.Frame(buttons_frame) checkboxes_frame.pack(side=tk.LEFT, padx=(8, 8)) # Create a checkbox for toggling parameter display @@ -269,8 +272,8 @@ def __create_parameter_area_widgets(self): "The files will be bigger, but all the existing parameter documentation will be included inside") # Create upload button - upload_selected_button = tk.Button(buttons_frame, text="Upload selected params to FC, and advance to next param file", - command=self.on_upload_selected_click) + upload_selected_button = ttk.Button(buttons_frame, text="Upload selected params to FC, and advance to next param file", + command=self.on_upload_selected_click) upload_selected_button.configure(state='normal' if self.flight_controller.master else 'disabled') upload_selected_button.pack(side=tk.LEFT, padx=(8, 8)) # Add padding on both sides of the upload selected button show_tooltip(upload_selected_button, "Upload selected parameters to the flight controller and advance to the next " @@ -278,7 +281,7 @@ def __create_parameter_area_widgets(self): "to save them\nIt will reset the FC if necessary, re-download all parameters and validate their value") # Create skip button - skip_button = tk.Button(buttons_frame, text="Skip parameter file", command=self.on_skip_click) + skip_button = ttk.Button(buttons_frame, text="Skip parameter file", command=self.on_skip_click) skip_button.pack(side=tk.RIGHT, padx=(8, 8)) # Add right padding to the skip button show_tooltip(skip_button, "Skip to the next intermediate parameter file without uploading any changes to the flight " "controller\nIf changes have been made to the current file it will ask if you want to save them") @@ -303,7 +306,7 @@ def __do_tempcal_imu(self, selected_file:str): if filename: messagebox.showwarning("IMU temperature calibration", "Please wait, this can take a really long time and\n" "the GUI will be unresponsive until it finishes.") - self.tempcal_imu_progress_window = ProgressWindow(self.root, "Reading IMU calibration messages", + self.tempcal_imu_progress_window = ProgressWindow(self.main_frame, "Reading IMU calibration messages", "Please wait, this can take a long time") # Pass the selected filename to the IMUfit class IMUfit(filename, tempcal_imu_result_param_fullpath, False, False, False, False, @@ -356,7 +359,7 @@ def on_param_file_combobox_change(self, _event, forced: bool = False): self.repopulate_parameter_table(selected_file) def download_flight_controller_parameters(self, redownload: bool = False): - self.param_download_progress_window = ProgressWindow(self.root, ("Re-d" if redownload else "D") + \ + self.param_download_progress_window = ProgressWindow(self.main_frame, ("Re-d" if redownload else "D") + \ "ownloading FC parameters", "Downloaded {} of {} parameters") # Download all parameters from the flight controller self.flight_controller.fc_parameters = self.flight_controller.download_params( @@ -439,7 +442,7 @@ def __reset_and_reconnect(self, fc_reset_required, fc_reset_unsure): "(s) potentially require a reset\nDo you want to reset the ArduPilot?") if fc_reset_required: - self.reset_progress_window = ProgressWindow(self.root, "Resetting Flight Controller", + self.reset_progress_window = ProgressWindow(self.main_frame, "Resetting Flight Controller", "Waiting for {} of {} seconds") # Call reset_and_reconnect with a callback to update the reset progress bar and the progress message error_message = self.flight_controller.reset_and_reconnect(self.reset_progress_window.update_progress_bar) diff --git a/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py b/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py index 8371445..67f42de 100644 --- a/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py +++ b/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py @@ -49,6 +49,9 @@ def __init__(self, root, local_filesystem, parameter_editor): self.upload_checkbutton_var = {} self.at_least_one_param_edited = False + style = ttk.Style() + style.configure('narrow.TButton', padding=0, width=4, border=(0, 0, 0, 0)) + # Prepare a dictionary that maps variable names to their values # These variables are used by the forced_parameters and derived_parameters in *_configuration_steps.json files self.variables = local_filesystem.get_eval_variables() @@ -80,7 +83,7 @@ def repopulate(self, selected_file: str, different_params: dict, fc_parameters: "When selected, upload the new value to the flight controller", "Reason why respective parameter changed"] for i, header in enumerate(headers): - label = tk.Label(self.view_port, text=header) + label = ttk.Label(self.view_port, text=header) label.grid(row=0, column=i, sticky="ew") # Use sticky="ew" to make the label stretch horizontally show_tooltip(label, tooltips[i]) @@ -151,7 +154,8 @@ def __update_table(self, params, fc_parameters): column[6].grid(row=i, column=6, sticky="ew", padx=(0, 5)) # Add the "Add" button at the bottom of the table - add_button = tk.Button(self.view_port, text="Add", command=lambda: self.__on_parameter_add(fc_parameters)) + add_button = ttk.Button(self.view_port, text="Add", style='narrow.TButton', + command=lambda: self.__on_parameter_add(fc_parameters)) show_tooltip(add_button, f"Add a parameter to the {self.current_file} file") add_button.grid(row=len(params)+2, column=0, sticky="w", padx=0) @@ -170,16 +174,17 @@ def __update_table(self, params, fc_parameters): self.view_port.columnconfigure(6, weight=1) # Change Reason def __create_delete_button(self, param_name): - delete_button = tk.Button(self.view_port, text="Del", command=lambda: self.__on_parameter_delete(param_name)) + delete_button = ttk.Button(self.view_port, text="Del", style='narrow.TButton', + command=lambda: self.__on_parameter_delete(param_name)) show_tooltip(delete_button, f"Delete {param_name} from the {self.current_file} file") return delete_button def __create_parameter_name(self, param_name, param_metadata, doc_tooltip): is_calibration = param_metadata.get('Calibration', False) if param_metadata else False is_readonly = param_metadata.get('ReadOnly', False) if param_metadata else False - parameter_label = tk.Label(self.view_port, text=param_name + (" " * (16 - len(param_name))), + parameter_label = ttk.Label(self.view_port, text=param_name + (" " * (16 - len(param_name))), background="red" if is_readonly else "yellow" if is_calibration else - self.root.cget("background")) + ttk.Style(self.root).lookup('TFrame', 'background')) if doc_tooltip: show_tooltip(parameter_label, doc_tooltip) return parameter_label @@ -189,19 +194,19 @@ def __create_flightcontroller_value(self, fc_parameters, param_name, param_defau value_str = format(fc_parameters[param_name], '.6f').rstrip('0').rstrip('.') if param_default is not None and is_within_tolerance(fc_parameters[param_name], param_default.value): # If it matches, set the background color to light blue - flightcontroller_value = tk.Label(self.view_port, text=value_str, + flightcontroller_value = ttk.Label(self.view_port, text=value_str, background="light blue") else: # Otherwise, set the background color to the default color - flightcontroller_value = tk.Label(self.view_port, text=value_str) + flightcontroller_value = ttk.Label(self.view_port, text=value_str) else: - flightcontroller_value = tk.Label(self.view_port, text="N/A", background="orange") + flightcontroller_value = ttk.Label(self.view_port, text="N/A", background="orange") if doc_tooltip: show_tooltip(flightcontroller_value, doc_tooltip) return flightcontroller_value @staticmethod - def __update_new_value_entry_text(new_value_entry: tk.Entry, value: float, param_default): + def __update_new_value_entry_text(new_value_entry: ttk.Entry, value: float, param_default): new_value_entry.delete(0, tk.END) text = format(value, '.6f').rstrip('0').rstrip('.') new_value_entry.insert(0, text) @@ -228,7 +233,7 @@ def __create_new_value_entry(self, param_name, param, # pylint: disable=too-man param.value = self.local_filesystem.derived_parameters[self.current_file][param_name].value self.at_least_one_param_edited = True - new_value_entry = tk.Entry(self.view_port, width=10, justify=tk.RIGHT) + new_value_entry = ttk.Entry(self.view_port, width=10, justify=tk.RIGHT) ParameterEditorTable.__update_new_value_entry_text(new_value_entry, param.value, param_default) bitmask_dict = param_metadata.get('Bitmask', None) if param_metadata else None try: @@ -295,7 +300,7 @@ def update_label(): new_decimal_value = sum(1 << key for key in checked_keys) # Replace the close button with a read-only label displaying the current new_decimal_value - close_label = tk.Label(window, text=f"{param_name} Value: {new_decimal_value}", state='disabled') + close_label = ttk.Label(window, text=f"{param_name} Value: {new_decimal_value}", state='disabled') close_label.grid(row=len(bitmask_dict), column=0, pady=10) # Bind the on_close function to the window's WM_DELETE_WINDOW protocol @@ -309,7 +314,7 @@ def update_label(): window.wait_window() # Wait for the window to be closed def __create_unit_label(self, param_metadata): - unit_label = tk.Label(self.view_port, text=param_metadata.get('unit') if param_metadata else "") + unit_label = ttk.Label(self.view_port, text=param_metadata.get('unit') if param_metadata else "") unit_tooltip = param_metadata.get('unit_tooltip') if param_metadata else \ "No documentation available in apm.pdef.xml for this parameter" if unit_tooltip: @@ -318,7 +323,7 @@ def __create_unit_label(self, param_metadata): def __create_upload_checkbutton(self, param_name, fc_connected): self.upload_checkbutton_var[param_name] = tk.BooleanVar(value=fc_connected) - upload_checkbutton = tk.Checkbutton(self.view_port, variable=self.upload_checkbutton_var[param_name]) + upload_checkbutton = ttk.Checkbutton(self.view_port, variable=self.upload_checkbutton_var[param_name]) upload_checkbutton.configure(state='normal' if fc_connected else 'disabled') show_tooltip(upload_checkbutton, f'When selected upload {param_name} new value to the flight controller') return upload_checkbutton @@ -339,7 +344,7 @@ def __create_change_reason_entry(self, param_name, param, new_value_entry): param.comment = self.local_filesystem.derived_parameters[self.current_file][param_name].comment self.at_least_one_param_edited = True - change_reason_entry = tk.Entry(self.view_port, background="white") + change_reason_entry = ttk.Entry(self.view_port, background="white") change_reason_entry.insert(0, "" if param.comment is None else param.comment) if present_as_forced: change_reason_entry.config(state='disabled', background='light grey') @@ -455,7 +460,7 @@ def get_upload_selected_params(self, current_file: str): def generate_edit_widgets_focus_out(self): # Trigger the event for all entry widgets to ensure all changes are processed for widget in self.view_port.winfo_children(): - if isinstance(widget, tk.Entry): + if isinstance(widget, ttk.Entry): widget.event_generate("", when="now") def get_at_least_one_param_edited(self):