Skip to content

Commit 9b98ae0

Browse files
committed
feat(component editor): Implement simple UI complexity combobox
Refresh UI on complexity change Notes fields are special because they hang differently Do not display MCU series in simple mode when it has a known value already
1 parent db74fc2 commit 9b98ae0

7 files changed

+445
-10
lines changed

ardupilot_methodic_configurator/backend_filesystem_program_settings.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727

2828
from ardupilot_methodic_configurator import _
2929

30-
SETTINGS_DEFAULTS: dict[str, Union[int, bool]] = {
30+
SETTINGS_DEFAULTS: dict[str, Union[int, bool, str]] = {
3131
"Format version": 1,
3232
"auto_open_doc_in_browser": True,
3333
"annotate_docs_into_param_files": False,
34+
"gui_complexity": "simple", # simple or normal
3435
}
3536

3637

@@ -247,14 +248,14 @@ def set_display_usage_popup(ptype: str, value: bool) -> None:
247248
ProgramSettings.__set_settings_from_dict(settings)
248249

249250
@staticmethod
250-
def get_setting(setting: str) -> Union[int, bool]:
251+
def get_setting(setting: str) -> Union[int, bool, str]:
251252
if setting in SETTINGS_DEFAULTS:
252253
setting_default = SETTINGS_DEFAULTS[setting]
253254
return ProgramSettings.__get_settings_as_dict().get(setting, setting_default) # type: ignore[no-any-return]
254255
return False
255256

256257
@staticmethod
257-
def set_setting(setting: str, value: Union[int, bool]) -> None:
258+
def set_setting(setting: str, value: Union[int, bool, str]) -> None:
258259
if setting in SETTINGS_DEFAULTS:
259260
settings, _, _ = ProgramSettings.__get_settings_config()
260261
settings[setting] = value

ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,58 @@ def _json_type_to_python_type(self, json_type: str) -> type:
718718

719719
return type_mapping.get(json_type, str) # Default to str if unknown type
720720

721+
def modify_schema_for_mcu_series(self, is_optional: bool) -> None:
722+
"""
723+
Modify the schema to set x-is-optional property for MCU Series field based on its value.
724+
725+
Dynamically updates the schema to make the MCU Series field optional
726+
727+
Args:
728+
is_optional: If it should be marked as optional
729+
730+
"""
731+
if not self.schema:
732+
self.load_schema()
733+
734+
if not self.schema: # Still None after loading
735+
logging_error(_("Cannot modify schema: Schema could not be loaded"))
736+
return
737+
738+
# Navigate to the MCU Series field in the schema
739+
flight_controller_def = self.schema.get("definitions", {}).get("flightController", {})
740+
flight_controller_properties = None
741+
742+
# Check in allOf construct
743+
if "allOf" in flight_controller_def:
744+
for item in flight_controller_def["allOf"]:
745+
if "properties" in item and "Specifications" in item["properties"]:
746+
flight_controller_properties = item
747+
break
748+
749+
if not flight_controller_properties:
750+
logging_error(_("Could not find Specifications in flight controller schema"))
751+
return
752+
753+
specifications = flight_controller_properties["properties"]["Specifications"]
754+
755+
if "properties" in specifications and "MCU Series" in specifications["properties"]:
756+
try:
757+
mcu_series_field = specifications["properties"]["MCU Series"]
758+
759+
if is_optional:
760+
mcu_series_field["x-is-optional"] = True
761+
else:
762+
# Remove x-is-optional if it exists
763+
mcu_series_field.pop("x-is-optional", None)
764+
765+
logging_debug(
766+
_("Modified schema: MCU Series x-is-optional=%s to value=%u"),
767+
mcu_series_field.get("x-is-optional", False),
768+
is_optional,
769+
)
770+
except Exception as err: # pylint: disable=broad-exception-caught
771+
logging_error(_("Error modifying schema for MCU Series: %s"), str(err))
772+
721773

722774
def main() -> None:
723775
"""Main function for standalone execution."""

ardupilot_methodic_configurator/frontend_tkinter_component_editor.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
6262

6363
def __init__(self, version: str, local_filesystem: LocalFilesystem) -> None:
6464
ComponentEditorWindowBase.__init__(self, version, local_filesystem)
65+
# when only read from file and no FC is connected
66+
mcu = self.data_model.get_component_value(("Flight Controller", "Specifications", "MCU Series"))
67+
if mcu and isinstance(mcu, str):
68+
self.set_mcu_series(mcu)
6569

6670
def set_vehicle_type_and_version(self, vehicle_type: str, version: str) -> None:
6771
"""Set the vehicle type and version in the data model."""
@@ -87,6 +91,8 @@ def set_mcu_series(self, mcu: str) -> None:
8791
# Update UI if widget exists
8892
if mcu:
8993
self.set_component_value_and_update_ui(("Flight Controller", "Specifications", "MCU Series"), mcu)
94+
if mcu.upper() in ("STM32F4XX", "STM32F7XX", "STM32H7XX"):
95+
self.local_filesystem.modify_schema_for_mcu_series(is_optional=True)
9096

9197
def set_vehicle_configuration_template(self, configuration_template: str) -> None:
9298
"""Set the configuration template name in the data."""

ardupilot_methodic_configurator/frontend_tkinter_component_editor_base.py

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from ardupilot_methodic_configurator import _, __version__
2626
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
27+
from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
2728
from ardupilot_methodic_configurator.common_arguments import add_common_arguments
2829
from ardupilot_methodic_configurator.data_model_vehicle_components import ComponentDataModel
2930
from ardupilot_methodic_configurator.data_model_vehicle_components_base import (
@@ -108,6 +109,7 @@ def __init__(
108109
self.scroll_frame: ScrollFrame
109110
self.save_button: ttk.Button
110111
self.template_manager: ComponentTemplateManager
112+
self.complexity_var = tk.StringVar()
111113

112114
# Initialize UI if there's data to work with
113115
if self._check_data():
@@ -175,11 +177,55 @@ def _add_explanation_text(self, parent: ttk.Frame) -> None:
175177
explanation_text += _("Labels for optional properties are displayed in gray text.\n")
176178
explanation_text += _("Scroll down to ensure that you do not overlook any properties.\n")
177179

180+
explanation_frame = ttk.Frame(parent)
181+
explanation_frame.pack(side=tk.TOP, fill=tk.X, padx=(10, 10), pady=(10, 0))
182+
178183
explanation_label = ttk.Label(
179-
parent, text=explanation_text, wraplength=WINDOW_WIDTH_PIX - VEICLE_IMAGE_WIDTH_PIX, justify=tk.LEFT
184+
explanation_frame, text=explanation_text, wraplength=WINDOW_WIDTH_PIX - VEICLE_IMAGE_WIDTH_PIX, justify=tk.LEFT
180185
)
181186
explanation_label.configure(style="bigger.TLabel")
182-
explanation_label.pack(side=tk.LEFT, padx=(10, 10), pady=(10, 0), anchor=tk.NW)
187+
explanation_label.pack(side=tk.LEFT, anchor=tk.NW)
188+
189+
# Add UI complexity combobox
190+
complexity_frame = ttk.Frame(explanation_frame)
191+
complexity_frame.pack(side=tk.RIGHT, anchor=tk.NE, padx=(0, 10))
192+
193+
complexity_label = ttk.Label(complexity_frame, text=_("GUI Complexity:"))
194+
complexity_label.pack(side=tk.LEFT, padx=(0, 5))
195+
196+
complexity_setting = ProgramSettings.get_setting("gui_complexity")
197+
self.complexity_var.set(str(complexity_setting) if complexity_setting is not None else "simple")
198+
199+
complexity_combobox = ttk.Combobox(
200+
complexity_frame, textvariable=self.complexity_var, values=["simple", "normal"], state="readonly", width=10
201+
)
202+
complexity_combobox.pack(side=tk.LEFT)
203+
complexity_combobox.bind("<<ComboboxSelected>>", self._on_complexity_changed)
204+
205+
# Add tooltip
206+
show_tooltip(
207+
complexity_combobox,
208+
_(
209+
"Select the graphical user interface complexity level:\n"
210+
"simple for beginners, only minimal mandatory fields no optional params displayed\n"
211+
"normal for everybody else."
212+
),
213+
)
214+
215+
def _on_complexity_changed(self, _event: Optional[tk.Event] = None) -> None:
216+
"""Handle complexity combobox change."""
217+
# Save the complexity setting
218+
ProgramSettings.set_setting("gui_complexity", self.complexity_var.get())
219+
220+
# Clear all widgets from the scroll frame
221+
for widget in self.scroll_frame.view_port.winfo_children():
222+
widget.destroy()
223+
224+
# Repopulate the frame with widgets according to the new complexity setting
225+
self.populate_frames()
226+
227+
# Update the UI
228+
self.scroll_frame.view_port.update_idletasks()
183229

184230
def _add_vehicle_image(self, parent: ttk.Frame) -> None:
185231
"""Add the vehicle image to the parent frame."""
@@ -293,6 +339,17 @@ def _add_widget(self, parent: tk.Widget, key: str, value: ComponentValue, path:
293339
path (list): The path to the current key in the JSON data.
294340
295341
"""
342+
# Skip components that shouldn't be displayed in simple mode
343+
if isinstance(value, dict) and not self._should_display_in_simple_mode(key, value, path):
344+
return
345+
346+
# For leaf nodes in simple mode, check if they are optional
347+
if not isinstance(value, dict) and self.complexity_var.get() == "simple":
348+
current_path = (*path, key)
349+
_, is_optional = self.local_filesystem.get_component_property_description(current_path)
350+
if is_optional:
351+
return
352+
296353
if isinstance(value, dict): # JSON non-leaf elements, add LabelFrame widget
297354
self._add_non_leaf_widget(parent, key, value, path)
298355
else: # JSON leaf elements, add Entry widget
@@ -399,7 +456,8 @@ def _create_leaf_widget_ui(self, parent: tk.Widget, config: dict) -> None:
399456

400457
def _add_template_controls(self, parent_frame: ttk.LabelFrame, component_name: str) -> None:
401458
"""Add template controls for a component."""
402-
self.template_manager.add_template_controls(parent_frame, component_name)
459+
if self.complexity_var.get() != "simple":
460+
self.template_manager.add_template_controls(parent_frame, component_name)
403461

404462
def get_component_data_from_gui(self, component_name: str) -> ComponentData:
405463
"""Extract component data from GUI elements."""
@@ -533,6 +591,66 @@ def create_for_testing(
533591

534592
return instance
535593

594+
def _should_display_in_simple_mode(self, key: str, value: dict, path: list[str]) -> bool: # pylint: disable=too-many-branches
595+
"""
596+
Determine if a component should be displayed in simple mode.
597+
598+
In simple mode, only show components that have at least one non-optional parameter.
599+
600+
Args:
601+
key (str): The component key
602+
value (dict): The component value
603+
path (list): The path to the component
604+
605+
Returns:
606+
bool: True if the component should be displayed, False otherwise
607+
608+
"""
609+
# If not in simple mode, always display the component
610+
if self.complexity_var.get() != "simple":
611+
return True
612+
613+
has_non_optional = False
614+
615+
# Top-level components need special handling
616+
if not path and key in self.data_model.get_all_components():
617+
# Check if this component has any non-optional parameters
618+
for sub_key, sub_value in value.items():
619+
if isinstance(sub_value, dict):
620+
# For nested dictionaries, recursively check
621+
if self._should_display_in_simple_mode(sub_key, sub_value, [*path, key]):
622+
return True
623+
else:
624+
# For leaf nodes, check if they are optional
625+
current_path = (*path, key, sub_key)
626+
_, is_optional = self.local_filesystem.get_component_property_description(current_path)
627+
if not is_optional:
628+
return True
629+
return False
630+
631+
# For non-top-level components or leaf nodes
632+
if isinstance(value, dict):
633+
# Check if this component has any non-optional parameters
634+
for sub_key, sub_value in value.items():
635+
current_path = (*path, key, sub_key)
636+
if isinstance(sub_value, dict):
637+
if self._should_display_in_simple_mode(sub_key, sub_value, [*path, key]):
638+
has_non_optional = True
639+
break
640+
else:
641+
_, is_optional = self.local_filesystem.get_component_property_description(current_path)
642+
if not is_optional:
643+
has_non_optional = True
644+
break
645+
else:
646+
# Leaf node - check if it's optional
647+
current_path = (*path, key)
648+
_, is_optional = self.local_filesystem.get_component_property_description(current_path)
649+
if not is_optional:
650+
has_non_optional = True
651+
652+
return has_non_optional
653+
536654

537655
if __name__ == "__main__":
538656
args = argument_parser()

tests/test_backend_filesystem_program_settings.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,40 @@ def test_set_setting(self) -> None:
218218
mock_set_settings.reset_mock()
219219
ProgramSettings.set_setting("nonexistent_setting", value=True)
220220
mock_set_settings.assert_not_called()
221+
222+
def test_get_setting_gui_complexity(self) -> None:
223+
"""Test getting the GUI complexity setting."""
224+
with patch.object(ProgramSettings, "_ProgramSettings__get_settings_as_dict") as mock_get_settings:
225+
# Test when setting exists
226+
mock_get_settings.return_value = {
227+
"Format version": 1,
228+
"gui_complexity": "simple",
229+
}
230+
assert ProgramSettings.get_setting("gui_complexity") == "simple"
231+
232+
# Test default value when setting doesn't exist
233+
mock_get_settings.return_value = {"Format version": 1}
234+
235+
# We need to mock the SETTINGS_DEFAULTS dictionary for this test
236+
with patch(
237+
"ardupilot_methodic_configurator.backend_filesystem_program_settings.SETTINGS_DEFAULTS",
238+
{"Format version": 1, "gui_complexity": "normal"},
239+
):
240+
assert ProgramSettings.get_setting("gui_complexity") == "normal" # Default from SETTINGS_DEFAULTS
241+
242+
def test_set_setting_gui_complexity(self) -> None:
243+
"""Test setting the GUI complexity setting."""
244+
with (
245+
patch.object(ProgramSettings, "_ProgramSettings__get_settings_config") as mock_get_config,
246+
patch.object(ProgramSettings, "_ProgramSettings__set_settings_from_dict") as mock_set_settings,
247+
):
248+
mock_get_config.return_value = ({"Format version": 1, "gui_complexity": "normal"}, "pattern", "replacement")
249+
250+
# Test setting gui_complexity to simple
251+
ProgramSettings.set_setting("gui_complexity", value="simple")
252+
mock_set_settings.assert_called_with({"Format version": 1, "gui_complexity": "simple"})
253+
254+
# Test setting gui_complexity back to normal
255+
mock_set_settings.reset_mock()
256+
ProgramSettings.set_setting("gui_complexity", value="normal")
257+
mock_set_settings.assert_called_with({"Format version": 1, "gui_complexity": "normal"})

0 commit comments

Comments
 (0)