Skip to content

Commit

Permalink
IMPROVEMENT: Use the more modern ttk widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
amilcarlucas committed Jun 7, 2024
1 parent a91a5c1 commit 6aa84b3
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 122 deletions.
38 changes: 21 additions & 17 deletions MethodicConfigurator/frontend_tkinter_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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
Expand All @@ -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
16 changes: 9 additions & 7 deletions MethodicConfigurator/frontend_tkinter_component_editor_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
59 changes: 35 additions & 24 deletions MethodicConfigurator/frontend_tkinter_connection_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,19 @@ 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(),
self.previous_selection,
"FC connection",
state='readonly')
self.conn_selection_combobox.bind("<<ComboboxSelected>>", 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")

Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 6aa84b3

Please sign in to comment.