Skip to content

Commit 93f3e29

Browse files
tripcodedamilcarlucas
authored andcommitted
feat(parameter editor): Add bulk add suggested parameters button
This PR addresses issue #654, where adding parameters one-by-one via the GUI was inefficient and time-consuming when multiple related parameters needed to be added. A new “Add suggested parameters” button has been added to the Add Parameter dialog. - The button is disabled by default - It becomes enabled only when ≤ 15 filtered parameter suggestions are visible - When clicked, all currently suggested parameters are added at once - Existing single-parameter add behavior remains unchanged Closes #654
1 parent dcbd034 commit 93f3e29

File tree

6 files changed

+647
-3
lines changed

6 files changed

+647
-3
lines changed

ardupilot_methodic_configurator/data_model_parameter_editor.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,6 +1769,38 @@ def add_parameter_to_current_file(self, param_name: str) -> bool:
17691769
)
17701770
return False
17711771

1772+
def bulk_add_parameters(self, param_names: list[str]) -> tuple[list[str], list[str], list[str]]:
1773+
"""
1774+
Add multiple parameters at once.
1775+
1776+
Args:
1777+
param_names: List of parameter names to add
1778+
1779+
Returns:
1780+
Tuple of (added, skipped, failed) parameter names
1781+
- added: Parameters successfully added
1782+
- skipped: Parameters that returned False from add_parameter_to_current_file
1783+
- failed: Parameters that raised InvalidParameterNameError or OperationNotPossibleError
1784+
(includes already existing parameters, invalid names, or missing data sources)
1785+
1786+
Business logic extracted for testability.
1787+
1788+
"""
1789+
added: list[str] = []
1790+
skipped: list[str] = []
1791+
failed: list[str] = []
1792+
1793+
for param_name in param_names:
1794+
try:
1795+
if self.add_parameter_to_current_file(param_name):
1796+
added.append(param_name)
1797+
else:
1798+
skipped.append(param_name)
1799+
except (InvalidParameterNameError, OperationNotPossibleError): # noqa: PERF203
1800+
failed.append(param_name)
1801+
1802+
return added, skipped, failed
1803+
17721804
def should_display_bitmask_parameter_editor_usage(self, param_name: str) -> bool:
17731805
return self.current_step_parameters[param_name].is_editable and self.current_step_parameters[param_name].is_bitmask
17741806

ardupilot_methodic_configurator/frontend_tkinter_entry_dynamic.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ def unpost_listbox(self, _event: Union[None, tk.Event] = None) -> str:
178178
def get_value(self) -> str:
179179
return self._entry_var.get() # type: ignore[no-any-return] # mypy bug
180180

181+
def get_filtered_items(self) -> list[str]:
182+
"""Get the list of currently filtered items in the listbox."""
183+
if self._listbox is None:
184+
return []
185+
return [self._listbox.get(i) for i in range(self._listbox.size())]
186+
181187
def set_value(self, text: str, close_dialog: bool = False) -> None:
182188
self._set_var(text)
183189

ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_table.py

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
ask_yesno_popup,
3535
show_error_popup,
3636
show_info_popup,
37+
show_warning_popup,
3738
)
3839
from ardupilot_methodic_configurator.frontend_tkinter_entry_dynamic import EntryWithDynamicalyFilteredListbox
3940
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import (
@@ -54,6 +55,14 @@
5455
NEW_VALUE_WIDGET_WIDTH = 9
5556
NEW_VALUE_DIFFERENT_STR = "\u2260" if platform_system() == "Windows" else "!="
5657

58+
# Maximum number of suggestions for bulk add button to be enabled.
59+
# When more than these suggestions are found, no bulk parameter add is offered.
60+
# This threshold prevents users from being overwhelmed by accidentally adding
61+
# too many parameters at once, which could lead to configuration errors or
62+
# difficulty tracking changes. 15 is chosen as a balance between convenience
63+
# and safety, allowing batch operations while maintaining user control.
64+
MAX_BULK_ADD_SUGGESTIONS = 15
65+
5766

5867
@dataclass
5968
class ParameterEditorTableDialogs:
@@ -790,11 +799,125 @@ def _on_parameter_delete(self, param_name: str) -> None:
790799
# Restore the scroll position
791800
self.canvas.yview_moveto(current_scroll_position)
792801

802+
@staticmethod
803+
def _calculate_bulk_add_button_state(suggestion_count: int) -> str:
804+
"""
805+
Calculate the state for the bulk add button based on suggestion count.
806+
807+
Args:
808+
suggestion_count: Number of filtered parameter suggestions
809+
810+
Returns:
811+
"normal" if 0 < suggestion_count <= MAX_BULK_ADD_SUGGESTIONS, otherwise "disabled"
812+
813+
Business logic extracted for testability.
814+
815+
"""
816+
if 0 < suggestion_count <= MAX_BULK_ADD_SUGGESTIONS:
817+
return "normal"
818+
return "disabled"
819+
820+
@staticmethod
821+
def _generate_bulk_add_feedback_message(added: list[str], skipped: list[str], failed: list[str]) -> tuple[str, str, str]:
822+
"""
823+
Generate feedback message for bulk parameter addition.
824+
825+
Args:
826+
added: List of successfully added parameter names
827+
skipped: List of skipped parameter names (already exist)
828+
failed: List of failed parameter names (invalid or errors)
829+
830+
Returns:
831+
Tuple of (message_type, title, message) where:
832+
- message_type: "success", "warning", "error", or "info"
833+
- title: The popup title
834+
- message: The detailed message text
835+
836+
Business logic extracted for testability.
837+
838+
"""
839+
# All parameters added successfully
840+
if added and not skipped and not failed:
841+
return "success", _("Success"), _("Successfully added %d parameter(s).") % len(added)
842+
843+
# Partial success - some added, some skipped or failed
844+
if added and (skipped or failed):
845+
msg_parts = [_("Added %d parameter(s).") % len(added)]
846+
if skipped:
847+
msg_parts.append(_("Skipped %d parameter(s): %s") % (len(skipped), ", ".join(skipped)))
848+
if failed:
849+
msg_parts.append(_("Failed %d parameter(s): %s") % (len(failed), ", ".join(failed)))
850+
return "warning", _("Partial Success"), "\n".join(msg_parts)
851+
852+
# All parameters already exist (skipped only)
853+
if skipped and not added and not failed:
854+
return "info", _("No Changes"), _("All %d parameter(s) already exist in the file.") % len(skipped)
855+
856+
# All parameters failed (no adds or skips)
857+
if failed and not added and not skipped:
858+
return "error", _("Error"), _("Failed to add all %d parameter(s): %s") % (len(failed), ", ".join(failed))
859+
860+
# Mixed skipped and failed, but nothing added
861+
if (skipped or failed) and not added:
862+
msg_parts = []
863+
if skipped:
864+
msg_parts.append(_("Skipped %d parameter(s): %s") % (len(skipped), ", ".join(skipped)))
865+
if failed:
866+
msg_parts.append(_("Failed %d parameter(s): %s") % (len(failed), ", ".join(failed)))
867+
return "error", _("No Parameters Added"), "\n".join(msg_parts)
868+
869+
# Fallback for unexpected state
870+
logging_critical("Unexpected bulk add result - added: %s, skipped: %s, failed: %s", added, skipped, failed)
871+
return "error", _("Error"), _("Unexpected result during bulk parameter addition.")
872+
873+
def _bulk_add_parameters_and_show_feedback(self, param_names: list[str], dialog_window: BaseWindow) -> None:
874+
"""
875+
Execute bulk parameter addition and display feedback to user.
876+
877+
Args:
878+
param_names: List of parameter names to add (should be uppercase)
879+
dialog_window: The add parameter dialog window to close after operation
880+
881+
Side Effects:
882+
- Adds parameters to the current configuration file
883+
- Updates the parameter editor table display
884+
- Scrolls to bottom if parameters were added
885+
- Displays feedback popup based on operation results
886+
- Closes the dialog window
887+
888+
Note:
889+
This method orchestrates the UI workflow. The business logic is in
890+
ParameterEditor.bulk_add_parameters() and message generation is in
891+
_generate_bulk_add_feedback_message() for better testability.
892+
893+
"""
894+
# Call business logic method
895+
added, skipped, failed = self.parameter_editor.bulk_add_parameters(param_names)
896+
897+
# Update UI if parameters were added
898+
if added:
899+
self._pending_scroll_to_bottom = True
900+
self.parameter_editor_window.repopulate_parameter_table()
901+
902+
# Generate and show feedback message
903+
message_type, title, message = self._generate_bulk_add_feedback_message(added, skipped, failed)
904+
905+
if message_type == "success":
906+
pass # Silent success - no popup needed for clean operation
907+
elif message_type == "warning":
908+
show_warning_popup(title, message)
909+
elif message_type == "info":
910+
show_info_popup(title, message)
911+
elif message_type == "error":
912+
show_error_popup(title, message)
913+
914+
dialog_window.root.destroy()
915+
793916
def _on_parameter_add(self) -> None:
794917
"""Handle parameter addition."""
795918
add_parameter_window = BaseWindow(self._get_parent_root())
796919
add_parameter_window.root.title(_("Add Parameter to ") + self.parameter_editor.current_file)
797-
add_parameter_window.root.geometry("450x300")
920+
add_parameter_window.root.geometry("450x320")
798921

799922
# Label for instruction
800923
instruction_label = ttk.Label(add_parameter_window.main_frame, text=_("Enter the parameter name to add:"))
@@ -819,6 +942,41 @@ def _on_parameter_add(self) -> None:
819942
BaseWindow.center_window(add_parameter_window.root, self._get_parent_toplevel())
820943
parameter_name_combobox.focus()
821944

945+
# Add all suggested parameters button (disabled by default)
946+
add_suggested_button = ttk.Button(
947+
add_parameter_window.main_frame,
948+
text=_("Add all suggested parameters"),
949+
state="disabled",
950+
)
951+
add_suggested_button.pack(pady=(10, 0))
952+
953+
# --- Helper: get filtered suggestions
954+
def get_filtered_parameter_names() -> list[str]:
955+
return [name.upper() for name in parameter_name_combobox.get_filtered_items()]
956+
957+
# --- Enable button only when <= MAX_BULK_ADD_SUGGESTIONS suggestions
958+
def update_add_suggested_button_state() -> None:
959+
filtered = get_filtered_parameter_names()
960+
button_state = self._calculate_bulk_add_button_state(len(filtered))
961+
add_suggested_button.configure(state=button_state)
962+
963+
# --- Bulk add handler
964+
def on_add_suggested_parameters() -> None:
965+
filtered = get_filtered_parameter_names()
966+
self._bulk_add_parameters_and_show_feedback(filtered, add_parameter_window)
967+
968+
add_suggested_button.configure(command=on_add_suggested_parameters)
969+
970+
# Update button state while typing
971+
parameter_name_combobox.bind(
972+
"<KeyRelease>",
973+
lambda _event: update_add_suggested_button_state(),
974+
)
975+
976+
# Initialize button state on dialog open
977+
update_add_suggested_button_state()
978+
979+
# --- Single-add behavior
822980
def custom_selection_handler(event: tk.Event) -> None:
823981
parameter_name_combobox.update_entry_from_listbox(event)
824982
param_name = parameter_name_combobox.get().upper()
@@ -827,7 +985,7 @@ def custom_selection_handler(event: tk.Event) -> None:
827985
else:
828986
add_parameter_window.root.focus()
829987

830-
# Bindings to handle Enter press and selection while respecting original functionalities
988+
# Bindings to handle Enter press and selection
831989
parameter_name_combobox.bind("<Return>", custom_selection_handler)
832990
parameter_name_combobox.bind("<<ComboboxSelected>>", custom_selection_handler)
833991

0 commit comments

Comments
 (0)