Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ardupilot_methodic_configurator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor import ParameterEditorWindow
from ardupilot_methodic_configurator.frontend_tkinter_project_opener import VehicleProjectOpenerWindow
from ardupilot_methodic_configurator.frontend_tkinter_show import show_error_message
from ardupilot_methodic_configurator.frontend_tkinter_usage_popup_window import PopupWindow, UsagePopupWindow
from ardupilot_methodic_configurator.plugin_constants import PLUGIN_MOTOR_TEST
from ardupilot_methodic_configurator.plugin_factory import plugin_factory

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

# Display workflow explanation popup
if PopupWindow.should_display("workflow_explanation"):
popup_window = UsagePopupWindow.display_workflow_explanation()
popup_window.root.mainloop()

# Validate that all configured plugins are registered
if state.local_filesystem:
validate_plugin_registry(state.local_filesystem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def _get_settings_defaults(cls) -> dict[str, Union[int, bool, str, float, dict]]
return {
"Format version": 1,
"display_usage_popup": {
"workflow_explanation": True,
"component_editor": True,
"component_editor_validation": True,
"parameter_editor": True,
Expand Down Expand Up @@ -127,6 +128,11 @@ def application_logo_filepath() -> str:
package_path = importlib_files("ardupilot_methodic_configurator")
return str(package_path / "images" / "ArduPilot_logo.png")

@staticmethod
def workflow_image_filepath() -> str:
package_path = importlib_files("ardupilot_methodic_configurator")
return str(package_path / "images" / "AMC_general_workflow.png")

@staticmethod
def create_new_vehicle_dir(new_vehicle_dir: str) -> str:
# Check if the new vehicle directory already exists
Expand Down Expand Up @@ -305,7 +311,7 @@ def display_usage_popup(ptype: str) -> bool:

@staticmethod
def set_display_usage_popup(ptype: str, value: bool) -> None:
if ptype in {"component_editor", "component_editor_validation", "parameter_editor"}:
if ptype in {"workflow_explanation", "component_editor", "component_editor_validation", "parameter_editor"}:
settings = ProgramSettings._get_settings_as_dict()
settings["display_usage_popup"][ptype] = value
ProgramSettings._set_settings_from_dict(settings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def _create_usage_popup_checkbox(popup_type: str, text: str) -> None:
)
checkbox.pack(side=tk.TOP, anchor=tk.W)

_create_usage_popup_checkbox("workflow_explanation", _("General AMC workflow"))
_create_usage_popup_checkbox("component_editor", _("Component editor window introduction"))
_create_usage_popup_checkbox("component_editor_validation", _("Component editor window data validation"))
_create_usage_popup_checkbox("parameter_editor", _("Parameter file editor and uploader window"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@
from typing import Callable, Optional

from ardupilot_methodic_configurator import _
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow
from ardupilot_methodic_configurator.frontend_tkinter_font import (
create_scaled_font,
get_safe_font_config,
)
from ardupilot_methodic_configurator.frontend_tkinter_rich_text import RichText


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

@staticmethod
def display_workflow_explanation(parent: Optional[tk.Tk] = None) -> BaseWindow:
"""
Display the workflow explanation popup window.

This popup explains that AMC is not a ground control station and has a different workflow.
"""
# Create the popup window
popup_window = BaseWindow()
instructions = RichText(
popup_window.main_frame,
wrap=tk.WORD,
height=1,
bd=0,
background=ttk.Style(popup_window.root).lookup("TLabel", "background"),
font=create_scaled_font(get_safe_font_config(), 1.2),
)
instructions.insert(tk.END, _("This is not a ground control station and it has a different workflow:"))
UsagePopupWindow.setup_window(
popup_window,
_("ArduPilot Methodic Configurator - Workflow"),
"490x362",
instructions,
)

# Add the image
image_path = LocalFilesystem.workflow_image_filepath()
try:
image_label = popup_window.put_image_in_label(popup_window.main_frame, image_path, image_height=141)
image_label.pack(pady=(0, 10))
except FileNotFoundError:
# Fallback if image not found
fallback_label = ttk.Label(popup_window.main_frame, text=_("[Image not found: AMC_general_workflow.png]"))
fallback_label.pack(pady=(0, 10))

# Add the rich text
rich_text = RichText(
popup_window.main_frame,
wrap=tk.WORD,
height=1,
bd=0,
background=ttk.Style(popup_window.root).lookup("TLabel", "background"),
font=create_scaled_font(get_safe_font_config(), 1.2),
)
rich_text.insert(tk.END, _("see "))
rich_text.insert_clickable_link(
_("quick start guide"), "quickstart_link", "https://ardupilot.github.io/MethodicConfigurator/#quick-start"
)
rich_text.insert(tk.END, _(", "))
rich_text.insert_clickable_link(
_("YouTube tutorials"), "YouTube_link", "https://www.youtube.com/playlist?list=PL1oa0qoJ9W_89eMcn4x2PB6o3fyPbheA9"
)
rich_text.insert(tk.END, _(", "))
rich_text.insert_clickable_link(
_("usecases"), "usecases_link", "https://ardupilot.github.io/MethodicConfigurator/USECASES.html"
)
rich_text.insert(tk.END, _(" and "))
rich_text.insert_clickable_link(
_("usermanual."), "usermanual_link", "https://ardupilot.github.io/MethodicConfigurator/USERMANUAL.html"
)
rich_text.config(borderwidth=0, relief="flat", highlightthickness=0, state=tk.DISABLED)
rich_text.pack(padx=6, pady=10)

UsagePopupWindow.finalize_setup_window(parent, popup_window, "workflow_explanation", _("I understand this"))
return popup_window


class ConfirmationPopupWindow(PopupWindow):
"""
Expand Down
62 changes: 62 additions & 0 deletions tests/test__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
vehicle_directory_selection,
write_parameter_defaults,
)
from ardupilot_methodic_configurator.frontend_tkinter_usage_popup_window import (
PopupWindow,
UsagePopupWindow,
)

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

Expand Down Expand Up @@ -203,6 +207,64 @@ def test_user_proceeds_normally_when_application_is_current(self, application_st
# Assert: Application continues normally
assert should_exit is False

def test_user_sees_workflow_explanation_popup_on_first_startup(self) -> None:
"""
User sees workflow explanation popup when starting application for the first time.

GIVEN: A user starts the application for the first time
WHEN: The application initializes
THEN: The workflow explanation popup should be displayed to guide them
AND: The popup should explain that AMC is not a ground control station
"""
# Arrange: Mock popup display enabled (default behavior)
with (
patch(
"ardupilot_methodic_configurator.__main__.PopupWindow.should_display",
return_value=True,
) as mock_should_display,
patch(
"ardupilot_methodic_configurator.__main__.UsagePopupWindow.display_workflow_explanation"
) as mock_display_popup,
):
mock_popup_window = MagicMock()
mock_display_popup.return_value = mock_popup_window

# Act: Simulate the startup popup logic from main()
if PopupWindow.should_display("workflow_explanation"):
UsagePopupWindow.display_workflow_explanation()
# Note: We don't call mainloop() in tests to avoid blocking

# Assert: User preference was checked
mock_should_display.assert_called_once_with("workflow_explanation")

# Assert: Popup was displayed for user guidance
mock_display_popup.assert_called_once()

def test_user_can_skip_workflow_popup_when_previously_disabled(self) -> None:
"""
User can skip workflow explanation popup when they have previously disabled it.

GIVEN: A user has previously chosen to disable the workflow popup
WHEN: The application starts
THEN: No popup should appear and application should proceed normally
"""
# Arrange: Mock popup display disabled by user preference
with patch(
"ardupilot_methodic_configurator.__main__.PopupWindow.should_display",
return_value=False,
) as mock_should_display:
popup_displayed = False

# Act: Simulate the startup popup logic from main()
if PopupWindow.should_display("workflow_explanation"):
popup_displayed = True

# Assert: User preference was checked
mock_should_display.assert_called_once_with("workflow_explanation")

# Assert: No popup was shown, respecting user preference
assert popup_displayed is False


class TestDocumentationBehavior:
"""Test automatic documentation opening behavior."""
Expand Down
19 changes: 19 additions & 0 deletions tests/test_backend_filesystem_program_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@ def test_user_can_get_logo_filepath_using_importlib_resources(self) -> None:
# Path should exist when running from source or installed package
assert os_path.exists(result), f"Logo file should exist at {result}"

def test_user_can_get_workflow_image_filepath_using_importlib_resources(self) -> None:
"""
User can retrieve workflow image filepath using modern importlib.resources method.

GIVEN: Python 3.9+ with importlib.resources.files available
WHEN: User requests the workflow image filepath for popup display
THEN: The path should be retrieved using importlib.resources
AND: The path should exist and end with AMC_general_workflow.png
"""
# Act: Get workflow image filepath (uses importlib.resources in Python 3.9+)
result = ProgramSettings.workflow_image_filepath()

# Assert: Path is valid and ends with expected filename
assert result.endswith("AMC_general_workflow.png")
assert "images" in result
# Path should exist when running from source or installed package
assert os_path.exists(result), f"Workflow image file should exist at {result}"


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

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