Skip to content

Commit 5b290a2

Browse files
committed
refactor(mouse wheel): Refactor mouse wheel handling in comboboxes
Add pytests Fix pylint Fix mypy
1 parent 7c980be commit 5b290a2

5 files changed

Lines changed: 352 additions & 81 deletions

ardupilot_methodic_configurator/frontend_tkinter_component_editor.py

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from logging import basicConfig as logging_basicConfig
1919
from logging import getLevelName as logging_getLevelName
2020
from tkinter import ttk
21-
from typing import Optional, Union
21+
from typing import Union
2222

2323
from ardupilot_methodic_configurator import _, __version__
2424
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
@@ -29,6 +29,7 @@
2929
FC_CONNECTION_TYPE_PATHS,
3030
)
3131
from ardupilot_methodic_configurator.frontend_tkinter_component_editor_base import ComponentEditorWindowBase
32+
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import setup_combobox_mousewheel_handling
3233

3334
# from ardupilot_methodic_configurator.frontend_tkinter_show import show_tooltip
3435
from ardupilot_methodic_configurator.frontend_tkinter_show import show_error_message, show_warning_message
@@ -179,35 +180,16 @@ def on_validate_combobox(event: tk.Event) -> bool:
179180
cb.bind("<Return>", on_validate_combobox)
180181
cb.bind("<ButtonRelease>", on_validate_combobox)
181182

182-
# Prevent mouse wheel from changing value when dropdown is not open
183-
def handle_mousewheel(_event: tk.Event, widget: tk.Widget = cb) -> Optional[str]:
184-
# Check if dropdown is open by examining the combobox's state
185-
dropdown_is_open = getattr(widget, "dropdown_is_open", False)
186-
if not dropdown_is_open:
187-
widget.master.event_generate("<MouseWheel>", delta=_event.delta) # type: ignore[attr-defined]
188-
return "break" # Prevent default behavior
189-
return None # Allow default behavior when dropdown is open
190-
191-
# Set flag when dropdown opens or closes
192-
def dropdown_opened(_event: tk.Event, widget: tk.Widget = cb) -> None:
193-
widget.dropdown_is_open = True # type: ignore[attr-defined]
194-
195-
def dropdown_closed(_event: tk.Event, widget: tk.Widget = cb) -> None:
196-
widget.dropdown_is_open = False # type: ignore[attr-defined]
197-
198-
# Initialize the flag
199-
cb.dropdown_is_open = False # type: ignore[attr-defined]
200-
201-
# Bind to events for dropdown opening and closing
202-
cb.bind("<<ComboboxDropdown>>", dropdown_opened)
203-
cb.bind(
204-
"<FocusOut>",
205-
lambda e, p=path: (dropdown_closed(e), self._validate_combobox(e, p)), # type: ignore[misc,func-returns-value]
206-
)
207-
# Bind mouse wheel events
208-
cb.bind("<MouseWheel>", handle_mousewheel) # Windows mouse wheel
209-
cb.bind("<Button-4>", handle_mousewheel) # Linux mouse wheel up
210-
cb.bind("<Button-5>", handle_mousewheel) # Linux mouse wheel down
183+
# Set up mouse wheel handling to prevent unwanted value changes
184+
setup_combobox_mousewheel_handling(cb)
185+
186+
# Override the FocusOut binding to also handle validation
187+
def combined_focus_out(event: tk.Event) -> None:
188+
# First handle the dropdown closing logic (already done by setup function)
189+
# Then handle validation
190+
self._validate_combobox(event, path)
191+
192+
cb.bind("<FocusOut>", combined_focus_out, add="+")
211193

212194
if path in FC_CONNECTION_TYPE_PATHS:
213195
cb.bind( # immediate update of Protocol combobox choices after changing connection Type selection

ardupilot_methodic_configurator/frontend_tkinter_pair_tuple_combobox.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,54 @@
2222
from platform import system as platform_system
2323
from sys import exit as sys_exit
2424
from tkinter import Label, Toplevel, ttk
25-
from typing import Union
25+
from typing import Optional, Union
2626

2727
from ardupilot_methodic_configurator import _
2828
from ardupilot_methodic_configurator.common_arguments import add_common_arguments
2929
from ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox import update_combobox_width
3030

31+
32+
def setup_combobox_mousewheel_handling(combobox: ttk.Combobox) -> None:
33+
"""
34+
Set up mouse wheel handling for a combobox to prevent value changes when scrolling.
35+
36+
This function adds event bindings to prevent the combobox value from changing
37+
when the user scrolls the mouse wheel over it while the dropdown is closed.
38+
When the dropdown is closed, the mouse wheel event is propagated to the parent
39+
widget to allow normal scrolling behavior in the containing widget.
40+
41+
Args:
42+
combobox: The ttk.Combobox widget to configure
43+
44+
"""
45+
# Track dropdown state to control mouse wheel behavior
46+
combobox.dropdown_is_open = False # type: ignore[attr-defined]
47+
48+
def handle_mousewheel(event: tk.Event) -> Optional[str]:
49+
"""Handle mouse wheel events - propagate to parent when dropdown is closed."""
50+
if not combobox.dropdown_is_open: # type: ignore[attr-defined]
51+
# Propagate the wheel event to the parent widget
52+
combobox.master.event_generate("<MouseWheel>", delta=event.delta)
53+
return "break" # Prevent default combobox behavior
54+
return None # Allow default behavior when dropdown is open
55+
56+
def dropdown_opened(_event: Optional[tk.Event] = None) -> None:
57+
"""Mark dropdown as open."""
58+
combobox.dropdown_is_open = True # type: ignore[attr-defined]
59+
60+
def dropdown_closed(_event: Optional[tk.Event] = None) -> None:
61+
"""Mark dropdown as closed."""
62+
combobox.dropdown_is_open = False # type: ignore[attr-defined] # Bind mouse wheel events (Windows and Linux)
63+
64+
combobox.bind("<MouseWheel>", handle_mousewheel, add="+")
65+
combobox.bind("<Button-4>", handle_mousewheel, add="+")
66+
combobox.bind("<Button-5>", handle_mousewheel, add="+")
67+
68+
# Track dropdown open/close events
69+
combobox.bind("<<ComboboxDropdown>>", dropdown_opened, add="+")
70+
combobox.bind("<FocusOut>", dropdown_closed, add="+")
71+
72+
3173
# SPDX-SnippetBegin
3274
# SPDX-License-Identifier: MPL-2.0
3375
# SPDX-SnippetCopyrightText: 2022 geraldew
@@ -58,6 +100,9 @@ def __init__(
58100
self.set_entries_tuple(list_pair_tuple, selected_element)
59101
self.bind("<Configure>", self.on_combo_configure, add="+")
60102

103+
# Apply mouse wheel handling to this combobox instance
104+
setup_combobox_mousewheel_handling(self)
105+
61106
def set_entries_tuple(self, list_pair_tuple: list[tuple[str, str]], selected_element: Union[None, str]) -> None:
62107
if isinstance(list_pair_tuple, list):
63108
for tpl in list_pair_tuple:

ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_table.py

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from platform import system as platform_system
1717
from sys import exit as sys_exit
1818
from tkinter import messagebox, ttk
19-
from typing import Optional, Union
19+
from typing import Union
2020

2121
from ardupilot_methodic_configurator import _
2222
from ardupilot_methodic_configurator.annotate_params import Par
@@ -30,7 +30,10 @@
3030
from ardupilot_methodic_configurator.data_model_configuration_step import ConfigurationStepProcessor
3131
from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow
3232
from ardupilot_methodic_configurator.frontend_tkinter_entry_dynamic import EntryWithDynamicalyFilteredListbox
33-
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox
33+
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import (
34+
PairTupleCombobox,
35+
setup_combobox_mousewheel_handling,
36+
)
3437
from ardupilot_methodic_configurator.frontend_tkinter_rich_text import get_widget_font_family_and_size
3538
from ardupilot_methodic_configurator.frontend_tkinter_scroll_frame import ScrollFrame
3639
from ardupilot_methodic_configurator.frontend_tkinter_show import show_tooltip
@@ -358,37 +361,6 @@ def _update_combobox_style_on_selection( # pylint: disable=too-many-arguments,
358361
event.width = NEW_VALUE_WIDGET_WIDTH
359362
combobox_widget.on_combo_configure(event)
360363

361-
def _setup_combobox_mousewheel_handling(self, combobox: PairTupleCombobox) -> None:
362-
"""Set up mouse wheel handling for combobox to prevent unwanted value changes."""
363-
364-
# Prevent mouse wheel from changing value when dropdown is not open
365-
def handle_mousewheel(_event: tk.Event, widget: tk.Widget = combobox) -> Optional[str]:
366-
# Check if dropdown is open by examining the combobox's state
367-
dropdown_is_open = getattr(widget, "dropdown_is_open", False)
368-
if not dropdown_is_open:
369-
widget.master.event_generate("<MouseWheel>", delta=_event.delta)
370-
return "break" # Prevent default behavior
371-
return None # Allow default behavior when dropdown is open
372-
373-
# Set flag when dropdown opens or closes
374-
def dropdown_opened(_event: tk.Event, widget: tk.Widget = combobox) -> None:
375-
widget.dropdown_is_open = True # type: ignore[attr-defined]
376-
377-
def dropdown_closed(_event: tk.Event, widget: tk.Widget = combobox) -> None:
378-
widget.dropdown_is_open = False # type: ignore[attr-defined]
379-
380-
# Initialize the flag
381-
combobox.dropdown_is_open = False # type: ignore[attr-defined]
382-
383-
# Bind to events for dropdown opening and closing
384-
combobox.bind("<<ComboboxDropdown>>", dropdown_opened)
385-
combobox.bind("<FocusOut>", dropdown_closed, "+")
386-
387-
# Bind mouse wheel events
388-
combobox.bind("<MouseWheel>", handle_mousewheel) # Windows mouse wheel
389-
combobox.bind("<Button-4>", handle_mousewheel) # Linux mouse wheel up
390-
combobox.bind("<Button-5>", handle_mousewheel) # Linux mouse wheel down
391-
392364
@staticmethod
393365
def _update_new_value_entry_text(new_value_entry: ttk.Entry, param: ArduPilotParameter) -> None:
394366
"""Update the new value entry text and style."""
@@ -442,7 +414,7 @@ def _create_new_value_entry( # pylint: disable=too-many-statements # noqa: PLR0
442414
)
443415

444416
# Set up mouse wheel handling to prevent unwanted value changes
445-
self._setup_combobox_mousewheel_handling(new_value_entry)
417+
setup_combobox_mousewheel_handling(new_value_entry)
446418
else:
447419
new_value_entry = ttk.Entry(self.view_port, width=NEW_VALUE_WIDGET_WIDTH + 1, justify=tk.RIGHT)
448420
self._update_new_value_entry_text(new_value_entry, param)

0 commit comments

Comments
 (0)