Same timer and lazy popup creation for all OS#1573
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the Tkinter Tooltip implementation to use delayed/on-demand creation (“lazy loading”) aimed at reducing UI overhead/flicker in dense/large tables, and adjusts the corresponding BDD tests.
Changes:
- Unify tooltip behavior across platforms by scheduling tooltip creation via
after()and creating theToplevelonly when needed. - Remove the old non-macOS eager-create + show/hide flow (including hide-delay-based behavior).
- Update tooltip-related tests to match the new lazy-loading and destroy-on-leave behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
ardupilot_methodic_configurator/frontend_tkinter_show.py |
Refactors Tooltip to lazy-create and destroy tooltips across platforms; removes legacy hide/show flow. |
tests/bdd_frontend_tkinter_show.py |
Updates tooltip tests to assert lazy creation, new bindings, and destroy-based hiding. |
☂️ Python Coverage
Overall Coverage
New FilesNo new covered files... Modified FilesNo covered modified files...
|
Test Results 4 files ± 0 4 suites ±0 40m 49s ⏱️ -41s Results for commit 1304eec. ± Comparison against base commit 7c0e6fd. This pull request removes 2 and adds 10 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
43b3829 to
6756141
Compare
6756141 to
59da953
Compare
| return # Avoid redundant tooltip creation | ||
|
|
||
| self.tooltip = cast("tk.Toplevel", self.toplevel_class(self.widget, bg="#ffffe0")) | ||
| self.tooltip = cast("tk.Toplevel", self.toplevel_class(self.widget)) |
There was a problem hiding this comment.
This changes the toplevel_class calling convention (previously toplevel_class(widget, bg=...), now only toplevel_class(widget) + configure). If external callers provided a custom toplevel_class that requires bg (or other kwargs) at construction time, this becomes a breaking change. A more compatible approach is to attempt construction with the previous kwargs and fall back to the simpler call on TypeError, while still keeping configure() for consistency.
| self.tooltip = cast("tk.Toplevel", self.toplevel_class(self.widget)) | |
| try: | |
| self.tooltip = cast("tk.Toplevel", self.toplevel_class(self.widget, bg="#ffffe0")) | |
| except TypeError: | |
| self.tooltip = cast("tk.Toplevel", self.toplevel_class(self.widget)) |
| @@ -588,11 +554,9 @@ def create_show(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002 | |||
| "help", | |||
| "noActivates", | |||
| ) | |||
| self.tooltip.configure(bg="#ffffe0") | |||
| except AttributeError: # Catches protected member access error | |||
| except (AttributeError, tk.TclError): # Fallback when MacWindowStyle or Tk attribute access is unsupported | |||
| self.tooltip.wm_attributes("-alpha", 1.0) # Ensure opacity | |||
| self.tooltip.wm_attributes("-topmost", True) # Keep on top # noqa: FBT003 | |||
There was a problem hiding this comment.
The MacWindowStyle call is attempted unconditionally for every platform, and the fallback unconditionally calls wm_attributes('-alpha', ...) / ('-topmost', ...). On non-macOS (or on Tk builds/WM configs that don't support these attributes), wm_attributes can raise tk.TclError and crash tooltip creation. Consider gating the MacWindowStyle block behind self._is_aqua (or at least platform_system() == 'Darwin'), and/or suppressing/handling tk.TclError around the wm_attributes fallback as well.
| import logging | ||
| import time | ||
| from typing import NamedTuple | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| from ardupilot_methodic_configurator.frontend_tkinter_show import MonitorBounds, Tooltip | ||
|
|
There was a problem hiding this comment.
Because this file lives under tests/ and matches pytest discovery naming (test_*.py), pytest will import it during collection even though it contains no tests. That can add unintended dependencies/import-time behavior to normal CI runs (and can fail in environments missing Tk). Consider moving it to a non-test location (e.g. benchmarks/ or scripts/) or renaming it so it’s not collected by default, and optionally integrate it via an explicit benchmark runner/marker.
| import logging | |
| import time | |
| from typing import NamedTuple | |
| from unittest.mock import MagicMock, patch | |
| from ardupilot_methodic_configurator.frontend_tkinter_show import MonitorBounds, Tooltip | |
| import importlib | |
| import logging | |
| import time | |
| from typing import NamedTuple | |
| from unittest.mock import MagicMock, patch | |
| class _LazyFrontendSymbol: | |
| """Lazily resolve symbols from the Tk frontend module when first used.""" | |
| def __init__(self, symbol_name: str) -> None: | |
| self._symbol_name = symbol_name | |
| def _resolve(self): | |
| module = importlib.import_module("ardupilot_methodic_configurator.frontend_tkinter_show") | |
| return getattr(module, self._symbol_name) | |
| def __call__(self, *args, **kwargs): | |
| return self._resolve()(*args, **kwargs) | |
| def __getattr__(self, item): | |
| return getattr(self._resolve(), item) | |
| MonitorBounds = _LazyFrontendSymbol("MonitorBounds") | |
| Tooltip = _LazyFrontendSymbol("Tooltip") |
|
|
||
| - **Before**: Tooltip already exists, just show/hide (microseconds) | ||
| - **After**: Tooltip created on-demand (2-3ms), then shown | ||
| - **Result**: First hover noticeable (~3-5ms delay), subsequent hovers instant |
There was a problem hiding this comment.
The doc claims 'subsequent hovers instant', but the current implementation destroys the tooltip on every leave (destroy_hide() → force_hide() → destroy()), so each hover will recreate the Toplevel again. Please update this section to reflect the actual lifecycle (on-demand creation happens on each hover unless a reuse/cache strategy is implemented).
| - **Result**: First hover noticeable (~3-5ms delay), subsequent hovers instant | |
| - **Result**: Each hover pays the on-demand creation cost (~3-5ms total) because the tooltip is destroyed on leave; subsequent hovers are only instant if a reuse/cache strategy is implemented |
07eed62 to
b826d77
Compare
b826d77 to
228fb9b
Compare
Coverage Report for CI Build 25103895234Warning No base build found for commit Coverage: 94.375%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats💛 - Coveralls |
…loading on large parameter table Signed-off-by: Omkar Sarkar <omkarsarkar24@gmail.com>
228fb9b to
1f468b7
Compare
190748d to
1304eec
Compare

Description
Concerns & Potential Issues⚠️
Breaking Change Without Validation: The commit removes macOS-specific tooltip behavior entirely. Questions:
Did you search for all usages of these methods before removing them?
If they were public API, this is a breaking change.
Test Coverage Gaps:
schedule_show() calls self.widget.after(TOOLTIP_SHOW_DELAY_MS, self.create_show)
What if the widget is destroyed before the timer fires? The _on_widget_destroy handler should catch this, but this edge case isn't tested.
Race Condition Possibility: In create_show(), the code checks if self.tooltip is None to avoid redundant creation, but:
Multiple rapid Enter/Leave events could queue multiple create_show calls
The timer cancellation logic may not fully prevent duplicate creations under edge cases
Incomplete Test Migration:
test_tooltip_do_hide_withdraws_on_non_macos was renamed to test_tooltip_force_hide_destroys_globally, but the semantics changed significantly
The old test verified non-macOS behavior; the new test claims to be cross-platform, but only tests the new behavior
Documentation:
Docstrings mention "on-demand load" but don't explain the performance implications
No migration guide for developers using the public Tooltip API
Critical Questions ❓
Checklist
git commit --signoff)Testing
Describe how you tested these changes: