forked from ArduPilot/MethodicConfigurator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
frontend_tkinter_component_editor_base.py
186 lines (145 loc) · 7.73 KB
/
frontend_tkinter_component_editor_base.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python3
'''
This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
(C) 2024 Amilcar do Carmo Lucas, IAV GmbH
SPDX-License-Identifier: GPL-3
'''
from argparse import ArgumentParser
from logging import basicConfig as logging_basicConfig
from logging import getLevelName as logging_getLevelName
# from logging import debug as logging_debug
from logging import info as logging_info
import tkinter as tk
from tkinter import ttk
from common_arguments import add_common_arguments_and_parse
from backend_filesystem import LocalFilesystem
from frontend_tkinter_base import show_tooltip
from frontend_tkinter_base import show_error_message
from frontend_tkinter_base import ScrollFrame
from frontend_tkinter_base import BaseWindow
from version import VERSION
def argument_parser():
"""
Parses command-line arguments for the script.
This function sets up an argument parser to handle the command-line arguments for the script.
Returns:
argparse.Namespace: An object containing the parsed arguments.
"""
parser = ArgumentParser(description='A GUI for editing JSON files that contain vehicle component configurations. '
'Not to be used directly, but through the main ArduPilot methodic configurator script.')
parser = LocalFilesystem.add_argparse_arguments(parser)
parser = ComponentEditorWindowBase.add_argparse_arguments(parser)
return add_common_arguments_and_parse(parser)
class ComponentEditorWindowBase(BaseWindow):
"""
A class for editing JSON files in the ArduPilot methodic configurator.
This class provides a graphical user interface for editing JSON files that
contain vehicle component configurations. It inherits from the BaseWindow
class, which provides basic window functionality.
"""
def __init__(self, version, local_filesystem: LocalFilesystem=None):
super().__init__()
self.local_filesystem = local_filesystem
self.root.title("Amilcar Lucas's - ArduPilot methodic configurator - " + version + " - Vehicle Component Editor")
self.root.geometry("880x600") # Set the window width
self.data = local_filesystem.load_vehicle_components_json_data(local_filesystem.vehicle_dir)
if len(self.data) < 1:
# Schedule the window to be destroyed after the mainloop has started
self.root.after(100, self.root.destroy) # Adjust the delay as needed
return
self.entry_widgets = {} # Dictionary for entry widgets
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(side=tk.TOP, fill="x", expand=False, pady=(4, 0)) # Pack the frame at the top of the window
# 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(self.main_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(self.main_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.pack(side="top", fill="both", expand=True)
self.__populate_frames()
self.save_button = ttk.Button(self.root, 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)
def __populate_frames(self):
"""
Populates the ScrollFrame with widgets based on the JSON data.
"""
if "Components" in self.data:
for key, value in self.data["Components"].items():
self.__add_widget(self.scroll_frame.view_port, key, value, [])
def __add_widget(self, parent, key, value, path):
"""
Adds a widget to the parent widget with the given key and value.
Parameters:
parent (tkinter.Widget): The parent widget to which the LabelFrame/Entry will be added.
key (str): The key for the LabelFrame/Entry.
value (dict): The value associated with the key.
path (list): The path to the current key in the JSON data.
"""
if isinstance(value, dict): # JSON non-leaf elements, add LabelFrame widget
frame = ttk.LabelFrame(parent, text=key)
is_toplevel = parent == self.scroll_frame.view_port
side = tk.TOP if is_toplevel else tk.LEFT
pady = 5 if is_toplevel else 3
anchor = tk.NW if is_toplevel else tk.N
frame.pack(fill=tk.X, side=side, pady=pady, padx=5, anchor=anchor)
for sub_key, sub_value in value.items():
# recursively add child elements
self.__add_widget(frame, sub_key, sub_value, path + [key])
else: # JSON leaf elements, add Entry widget
entry_frame = ttk.Frame(parent)
entry_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
label = ttk.Label(entry_frame, text=key)
label.pack(side=tk.LEFT)
entry = self.add_entry_or_combobox(value, entry_frame, tuple(path+[key]))
entry.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 5))
# Store the entry widget in the entry_widgets dictionary for later retrieval
self.entry_widgets[tuple(path+[key])] = entry
def save_data(self):
"""
Saves the edited JSON data back to the file.
"""
for path, entry in self.entry_widgets.items():
value = entry.get()
# Navigate through the nested dictionaries using the elements of the path
current_data = self.data["Components"]
for key in path[:-1]:
current_data = current_data[key]
if path[-1] != "Version":
try:
value = int(value)
except ValueError:
try:
value = float(value)
except ValueError:
value = str(value)
# Update the value in the data dictionary
current_data[path[-1]] = value
# Save the updated data back to the JSON file
if self.local_filesystem.save_vehicle_components_json_data(self.data, self.local_filesystem.vehicle_dir):
show_error_message("Error", "Failed to save data to file. Is the destination write protected?")
else:
logging_info("Data saved successfully.")
self.root.destroy()
# This function will be overwritten in child classes
def add_entry_or_combobox(self, value, entry_frame, path): # pylint: disable=unused-argument
entry = ttk.Entry(entry_frame)
entry.insert(0, str(value))
return entry
@staticmethod
def add_argparse_arguments(parser):
parser.add_argument('--skip-component-editor',
action='store_true',
help='Skip the component editor window. Only use this if all components have been configured. '
'Default to false')
return parser
if __name__ == "__main__":
args = argument_parser()
logging_basicConfig(level=logging_getLevelName(args.loglevel), format='%(asctime)s - %(levelname)s - %(message)s')
filesystem = LocalFilesystem(args.vehicle_dir, args.vehicle_type)
app = ComponentEditorWindowBase(VERSION, filesystem)
app.root.mainloop()