Skip to content

Commit 5f97180

Browse files
committed
feat(workflow popup): display workflow explanation popup
Added BDD tests as well
1 parent 9b8f0a3 commit 5f97180

7 files changed

Lines changed: 362 additions & 67 deletions

ardupilot_methodic_configurator/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor import ParameterEditorWindow
4848
from ardupilot_methodic_configurator.frontend_tkinter_project_opener import VehicleProjectOpenerWindow
4949
from ardupilot_methodic_configurator.frontend_tkinter_show import show_error_message
50+
from ardupilot_methodic_configurator.frontend_tkinter_usage_popup_window import PopupWindow, UsagePopupWindow
5051
from ardupilot_methodic_configurator.plugin_constants import PLUGIN_MOTOR_TEST
5152
from ardupilot_methodic_configurator.plugin_factory import plugin_factory
5253

@@ -566,6 +567,11 @@ def main() -> None:
566567
if check_updates(state):
567568
sys_exit(0) # user asked to update, exit the old version
568569

570+
# Display workflow explanation popup
571+
if PopupWindow.should_display("workflow_explanation"):
572+
popup_window = UsagePopupWindow.display_workflow_explanation()
573+
popup_window.root.mainloop()
574+
569575
# Validate that all configured plugins are registered
570576
if state.local_filesystem:
571577
validate_plugin_registry(state.local_filesystem)

ardupilot_methodic_configurator/backend_filesystem_program_settings.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def _get_settings_defaults(cls) -> dict[str, Union[int, bool, str, float, dict]]
5858
return {
5959
"Format version": 1,
6060
"display_usage_popup": {
61+
"workflow_explanation": True,
6162
"component_editor": True,
6263
"component_editor_validation": True,
6364
"parameter_editor": True,
@@ -127,6 +128,11 @@ def application_logo_filepath() -> str:
127128
package_path = importlib_files("ardupilot_methodic_configurator")
128129
return str(package_path / "images" / "ArduPilot_logo.png")
129130

131+
@staticmethod
132+
def workflow_image_filepath() -> str:
133+
package_path = importlib_files("ardupilot_methodic_configurator")
134+
return str(package_path / "images" / "AMC_general_workflow.png")
135+
130136
@staticmethod
131137
def create_new_vehicle_dir(new_vehicle_dir: str) -> str:
132138
# Check if the new vehicle directory already exists
@@ -305,7 +311,7 @@ def display_usage_popup(ptype: str) -> bool:
305311

306312
@staticmethod
307313
def set_display_usage_popup(ptype: str, value: bool) -> None:
308-
if ptype in {"component_editor", "component_editor_validation", "parameter_editor"}:
314+
if ptype in {"workflow_explanation", "component_editor", "component_editor_validation", "parameter_editor"}:
309315
settings = ProgramSettings._get_settings_as_dict()
310316
settings["display_usage_popup"][ptype] = value
311317
ProgramSettings._set_settings_from_dict(settings)

ardupilot_methodic_configurator/frontend_tkinter_about_popup_window.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def _create_usage_popup_checkbox(popup_type: str, text: str) -> None:
5858
)
5959
checkbox.pack(side=tk.TOP, anchor=tk.W)
6060

61+
_create_usage_popup_checkbox("workflow_explanation", _("General AMC workflow"))
6162
_create_usage_popup_checkbox("component_editor", _("Component editor window introduction"))
6263
_create_usage_popup_checkbox("component_editor_validation", _("Component editor window data validation"))
6364
_create_usage_popup_checkbox("parameter_editor", _("Parameter file editor and uploader window"))

ardupilot_methodic_configurator/frontend_tkinter_usage_popup_window.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@
1818
from typing import Callable, Optional
1919

2020
from ardupilot_methodic_configurator import _
21+
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
2122
from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
2223
from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow
24+
from ardupilot_methodic_configurator.frontend_tkinter_font import (
25+
create_scaled_font,
26+
get_safe_font_config,
27+
)
2328
from ardupilot_methodic_configurator.frontend_tkinter_rich_text import RichText
2429

2530

@@ -172,6 +177,72 @@ def display( # pylint: disable=too-many-arguments, too-many-positional-argument
172177
UsagePopupWindow.setup_window(usage_popup_window, title, geometry, instructions_text)
173178
UsagePopupWindow.finalize_setup_window(parent, usage_popup_window, ptype)
174179

180+
@staticmethod
181+
def display_workflow_explanation(parent: Optional[tk.Tk] = None) -> BaseWindow:
182+
"""
183+
Display the workflow explanation popup window.
184+
185+
This popup explains that AMC is not a ground control station and has a different workflow.
186+
"""
187+
# Create the popup window
188+
popup_window = BaseWindow()
189+
instructions = RichText(
190+
popup_window.main_frame,
191+
wrap=tk.WORD,
192+
height=1,
193+
bd=0,
194+
background=ttk.Style(popup_window.root).lookup("TLabel", "background"),
195+
font=create_scaled_font(get_safe_font_config(), 1.2),
196+
)
197+
instructions.insert(tk.END, _("This is not a ground control station and it has a different workflow:"))
198+
UsagePopupWindow.setup_window(
199+
popup_window,
200+
_("ArduPilot Methodic Configurator - Workflow"),
201+
"490x362",
202+
instructions,
203+
)
204+
205+
# Add the image
206+
image_path = LocalFilesystem.workflow_image_filepath()
207+
try:
208+
image_label = popup_window.put_image_in_label(popup_window.main_frame, image_path, image_height=141)
209+
image_label.pack(pady=(0, 10))
210+
except FileNotFoundError:
211+
# Fallback if image not found
212+
fallback_label = ttk.Label(popup_window.main_frame, text=_("[Image not found: AMC_general_workflow.png]"))
213+
fallback_label.pack(pady=(0, 10))
214+
215+
# Add the rich text
216+
rich_text = RichText(
217+
popup_window.main_frame,
218+
wrap=tk.WORD,
219+
height=1,
220+
bd=0,
221+
background=ttk.Style(popup_window.root).lookup("TLabel", "background"),
222+
font=create_scaled_font(get_safe_font_config(), 1.2),
223+
)
224+
rich_text.insert(tk.END, _("see "))
225+
rich_text.insert_clickable_link(
226+
_("quick start guide"), "quickstart_link", "https://ardupilot.github.io/MethodicConfigurator/#quick-start"
227+
)
228+
rich_text.insert(tk.END, _(", "))
229+
rich_text.insert_clickable_link(
230+
_("YouTube tutorials"), "YouTube_link", "https://www.youtube.com/playlist?list=PL1oa0qoJ9W_89eMcn4x2PB6o3fyPbheA9"
231+
)
232+
rich_text.insert(tk.END, _(", "))
233+
rich_text.insert_clickable_link(
234+
_("usecases"), "usecases_link", "https://ardupilot.github.io/MethodicConfigurator/USECASES.html"
235+
)
236+
rich_text.insert(tk.END, _(" and "))
237+
rich_text.insert_clickable_link(
238+
_("usermanual."), "usermanual_link", "https://ardupilot.github.io/MethodicConfigurator/USERMANUAL.html"
239+
)
240+
rich_text.config(borderwidth=0, relief="flat", highlightthickness=0, state=tk.DISABLED)
241+
rich_text.pack(padx=6, pady=10)
242+
243+
UsagePopupWindow.finalize_setup_window(parent, popup_window, "workflow_explanation", _("I understand this"))
244+
return popup_window
245+
175246

176247
class ConfirmationPopupWindow(PopupWindow):
177248
"""

tests/test__main__.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
vehicle_directory_selection,
3535
write_parameter_defaults,
3636
)
37+
from ardupilot_methodic_configurator.frontend_tkinter_usage_popup_window import (
38+
PopupWindow,
39+
UsagePopupWindow,
40+
)
3741

3842
# pylint: disable=,too-many-lines,redefined-outer-name,too-few-public-methods
3943

@@ -203,6 +207,64 @@ def test_user_proceeds_normally_when_application_is_current(self, application_st
203207
# Assert: Application continues normally
204208
assert should_exit is False
205209

210+
def test_user_sees_workflow_explanation_popup_on_first_startup(self) -> None:
211+
"""
212+
User sees workflow explanation popup when starting application for the first time.
213+
214+
GIVEN: A user starts the application for the first time
215+
WHEN: The application initializes
216+
THEN: The workflow explanation popup should be displayed to guide them
217+
AND: The popup should explain that AMC is not a ground control station
218+
"""
219+
# Arrange: Mock popup display enabled (default behavior)
220+
with (
221+
patch(
222+
"ardupilot_methodic_configurator.__main__.PopupWindow.should_display",
223+
return_value=True,
224+
) as mock_should_display,
225+
patch(
226+
"ardupilot_methodic_configurator.__main__.UsagePopupWindow.display_workflow_explanation"
227+
) as mock_display_popup,
228+
):
229+
mock_popup_window = MagicMock()
230+
mock_display_popup.return_value = mock_popup_window
231+
232+
# Act: Simulate the startup popup logic from main()
233+
if PopupWindow.should_display("workflow_explanation"):
234+
UsagePopupWindow.display_workflow_explanation()
235+
# Note: We don't call mainloop() in tests to avoid blocking
236+
237+
# Assert: User preference was checked
238+
mock_should_display.assert_called_once_with("workflow_explanation")
239+
240+
# Assert: Popup was displayed for user guidance
241+
mock_display_popup.assert_called_once()
242+
243+
def test_user_can_skip_workflow_popup_when_previously_disabled(self) -> None:
244+
"""
245+
User can skip workflow explanation popup when they have previously disabled it.
246+
247+
GIVEN: A user has previously chosen to disable the workflow popup
248+
WHEN: The application starts
249+
THEN: No popup should appear and application should proceed normally
250+
"""
251+
# Arrange: Mock popup display disabled by user preference
252+
with patch(
253+
"ardupilot_methodic_configurator.__main__.PopupWindow.should_display",
254+
return_value=False,
255+
) as mock_should_display:
256+
popup_displayed = False
257+
258+
# Act: Simulate the startup popup logic from main()
259+
if PopupWindow.should_display("workflow_explanation"):
260+
popup_displayed = True
261+
262+
# Assert: User preference was checked
263+
mock_should_display.assert_called_once_with("workflow_explanation")
264+
265+
# Assert: No popup was shown, respecting user preference
266+
assert popup_displayed is False
267+
206268

207269
class TestDocumentationBehavior:
208270
"""Test automatic documentation opening behavior."""

tests/test_backend_filesystem_program_settings.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,24 @@ def test_user_can_get_logo_filepath_using_importlib_resources(self) -> None:
131131
# Path should exist when running from source or installed package
132132
assert os_path.exists(result), f"Logo file should exist at {result}"
133133

134+
def test_user_can_get_workflow_image_filepath_using_importlib_resources(self) -> None:
135+
"""
136+
User can retrieve workflow image filepath using modern importlib.resources method.
137+
138+
GIVEN: Python 3.9+ with importlib.resources.files available
139+
WHEN: User requests the workflow image filepath for popup display
140+
THEN: The path should be retrieved using importlib.resources
141+
AND: The path should exist and end with AMC_general_workflow.png
142+
"""
143+
# Act: Get workflow image filepath (uses importlib.resources in Python 3.9+)
144+
result = ProgramSettings.workflow_image_filepath()
145+
146+
# Assert: Path is valid and ends with expected filename
147+
assert result.endswith("AMC_general_workflow.png")
148+
assert "images" in result
149+
# Path should exist when running from source or installed package
150+
assert os_path.exists(result), f"Workflow image file should exist at {result}"
151+
134152

135153
class TestDirectoryManagement:
136154
"""Test user directory creation and validation workflows."""
@@ -386,6 +404,7 @@ def test_user_can_load_existing_settings_file(self, mock_user_config, sample_pro
386404
expected_result["gui_complexity"] = "simple" # Added by default
387405
expected_result["motor_test"] = {"duration": 2, "throttle_pct": 10} # Added by default
388406
expected_result["display_usage_popup"]["component_editor_validation"] = True # Added by default
407+
expected_result["display_usage_popup"]["workflow_explanation"] = True # Added by default
389408

390409
# Update directory_selection with the defaults that would be merged in
391410
expected_result["directory_selection"]["new_base_dir"] = os_path.join(mock_user_config["config_dir"], "vehicles")

0 commit comments

Comments
 (0)