Skip to content

Fix window shuttering during initial loading by preventing short-lived windows #655

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
22 changes: 12 additions & 10 deletions ardupilot_methodic_configurator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def initialize_flight_controller_and_filesystem(state: ApplicationState) -> None

# Get default parameter values from flight controller
if state.flight_controller.master is not None or state.args.device == "test":
fciw = FlightControllerInfoWindow(state.flight_controller)
fciw = FlightControllerInfoWindow(state.flight_controller, show_window=False)
state.param_default_values = fciw.get_param_default_values()

# Initialize local filesystem
Expand Down Expand Up @@ -316,6 +316,16 @@ def component_editor(state: ApplicationState) -> None:
SystemExit: If there's an error in derived parameters

"""
# Handle skip component editor option early to avoid window creation
should_skip_editor = state.args.skip_component_editor and not (
state.vehicle_dir_window
and state.vehicle_dir_window.configuration_template
and state.vehicle_dir_window.blank_component_data.get()
)
if should_skip_editor:
# Skip creating the window entirely to avoid shuttering
return

# Create and configure the component editor window
component_editor_window = create_and_configure_component_editor(
__version__,
Expand All @@ -325,15 +335,7 @@ def component_editor(state: ApplicationState) -> None:
state.vehicle_dir_window,
)

# Handle skip component editor option
should_skip_editor = state.args.skip_component_editor and not (
state.vehicle_dir_window
and state.vehicle_dir_window.configuration_template
and state.vehicle_dir_window.blank_component_data.get()
)
if should_skip_editor:
component_editor_window.root.after(10, component_editor_window.root.destroy)
elif should_open_firmware_documentation(state.flight_controller):
if should_open_firmware_documentation(state.flight_controller):
open_firmware_documentation(state.flight_controller.info.firmware_type)

# Run the GUI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,23 @@
class FlightControllerInfoWindow(BaseWindow):
"""Display flight controller hardware, firmware and parameter information."""

def __init__(self, flight_controller: FlightController) -> None:
super().__init__()
self.root.title(_("ArduPilot methodic configurator ") + __version__ + _(" - Flight Controller Info"))
self.root.geometry("500x350") # Adjust the window size as needed

def __init__(self, flight_controller: FlightController, show_window: bool = True) -> None:
self.presenter = FlightControllerInfoPresenter(flight_controller)

self._create_info_display()
self.presenter.log_flight_controller_info()

Check failure on line 69 in ardupilot_methodic_configurator/frontend_tkinter_flightcontroller_info.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

ardupilot_methodic_configurator/frontend_tkinter_flightcontroller_info.py:69:1: W293 Blank line contains whitespace

Check failure on line 69 in ardupilot_methodic_configurator/frontend_tkinter_flightcontroller_info.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

ardupilot_methodic_configurator/frontend_tkinter_flightcontroller_info.py:69:1: W293 Blank line contains whitespace
if show_window:
super().__init__()
self.root.title(_("ArduPilot methodic configurator ") + __version__ + _(" - Flight Controller Info"))
self.root.geometry("500x350") # Adjust the window size as needed

# Schedule parameter download after UI is ready
self.root.after(50, self._download_flight_controller_parameters)
self.root.mainloop()
self._create_info_display()

# Schedule parameter download after UI is ready
self.root.after(50, self._download_flight_controller_parameters)
self.root.mainloop()
else:
# Download parameters without showing window
self._download_flight_controller_parameters_no_window()

def _create_info_display(self) -> None:
"""Create the flight controller information display."""
Expand Down Expand Up @@ -120,6 +124,14 @@
param_download_progress_window.destroy() # for the case that '--device test' and there is no real FC connected
self.root.destroy()

def _download_flight_controller_parameters_no_window(self) -> None:
"""Download flight controller parameters without showing progress window."""
try:
self.presenter.download_parameters()
except Exception as e: # pylint: disable=broad-exception-caught
# Log the error
logging.error("Failed to download parameters: %s", e)

def get_param_default_values(self) -> dict[str, Par]:
"""Get parameter default values from the presenter."""
return self.presenter.get_param_default_values()
13 changes: 5 additions & 8 deletions tests/test__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ def test_user_can_initialize_with_connected_hardware(
assert application_state.local_filesystem is mock_local_filesystem
assert application_state.param_default_values == default_params
assert application_state.param_default_values_dirty is True
mock_fc_window.assert_called_once_with(mock_flight_controller, show_window=False)

def test_user_can_work_in_simulation_mode(self, application_state: ApplicationState) -> None:
"""
Expand Down Expand Up @@ -318,7 +319,7 @@ def test_user_can_work_in_simulation_mode(self, application_state: ApplicationSt
# Assert: Simulation mode works
assert application_state.flight_controller is mock_fc
assert application_state.vehicle_type == "ArduCopter"
mock_fc_window.assert_called_once_with(mock_fc)
mock_fc_window.assert_called_once_with(mock_fc, show_window=False)

def test_user_receives_clear_error_when_configuration_invalid(self, application_state: ApplicationState) -> None:
"""
Expand Down Expand Up @@ -1139,7 +1140,7 @@ def test_component_editor_workflow_with_skip(self, application_state: Applicatio

GIVEN: User wants to skip component editor
WHEN: Component editor workflow runs
THEN: GUI should close automatically without user interaction
THEN: Window creation should be skipped entirely to prevent shuttering
"""
# Note: component_editor is already imported at the top of the file

Expand All @@ -1157,15 +1158,11 @@ def test_component_editor_workflow_with_skip(self, application_state: Applicatio
application_state.local_filesystem = mock_filesystem

with patch("ardupilot_methodic_configurator.__main__.create_and_configure_component_editor") as mock_create:
mock_window = MagicMock()
mock_create.return_value = mock_window

# Act: Run component editor with skip
component_editor(application_state)

# Assert: Window configured to auto-close
mock_window.root.after.assert_called_once_with(10, mock_window.root.destroy)
mock_window.root.mainloop.assert_called_once()
# Assert: Window creation is skipped entirely (no shuttering)
mock_create.assert_not_called()

def test_component_editor_workflow_with_documentation(self, application_state: ApplicationState) -> None:
"""
Expand Down
32 changes: 32 additions & 0 deletions tests/test_frontend_tkinter_flightcontroller_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,35 @@ def test_user_sees_formatted_flight_controller_information_display(
mock_ttk_frame.pack.assert_called_once()
mock_ttk_frame.columnconfigure.assert_called_once_with(1, weight=1)
configured_flight_controller.info.get_info.assert_called_once()

def test_user_can_download_parameters_without_showing_window(
self, configured_flight_controller: Mock
) -> None:
"""
Test that users can download parameters in background without UI disruption.

GIVEN: A flight controller with downloadable parameters
WHEN: A user creates a FlightControllerInfoWindow with show_window=False
THEN: Parameters are downloaded without displaying a window interface
"""
# Given - Configure the mock to return sample parameters
configured_flight_controller.download_params.return_value = (
{"FC_PARAM1": 1.0, "FC_PARAM2": 2.5},
{"PARAM1": Par(1.0, "test parameter 1"), "PARAM2": Par(2.5, "test parameter 2")},
)

# When - Create window in background mode
with (
patch("ardupilot_methodic_configurator.frontend_tkinter_flightcontroller_info.BaseWindow.__init__"),
patch("tkinter.Tk"),
patch("tkinter.Toplevel"),
patch("tkinter.ttk.Frame"),
):
window = FlightControllerInfoWindow(configured_flight_controller, show_window=False)

# Then - Parameters are downloaded without GUI creation
param_defaults = window.get_param_default_values()
assert param_defaults is not None
assert isinstance(param_defaults, dict)
assert "PARAM1" in param_defaults
configured_flight_controller.download_params.assert_called_once()
Loading