FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(native_views): drop ios control actions doubled within one tap · pythonnative/pythonnative@7ef22ae · GitHub

Repository navigation

Commit 7ef22ae

Browse files
committed
fix(native_views): drop ios control actions doubled within one tap
1 parent ec8aa23 commit 7ef22ae

1 file changed

Lines changed: 68 additions & 1 deletion

File tree

  • src/pythonnative/native_views

‎src/pythonnative/native_views/ios.py‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ def _set_recognizer_delegate(recognizer: Any) -> None:
754754
_pn_action_handlers: Dict[int, Any] = {}
755755

756756
_ACTION_IMP_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
757+
_CONTROL_ACTION_IMP_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
757758

758759

759760
def _action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int) -> None:
@@ -769,7 +770,65 @@ def _action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int) -> None:
769770

770771
_action_imp_ref = _ACTION_IMP_TYPE(_action_imp)
771772

773+
# ----------------------------------------------------------------------
774+
# UIControl action dedupe
775+
# ----------------------------------------------------------------------
776+
#
777+
# The simulator's XCTest event-injection stack sometimes delivers a
778+
# single synthesized tap as *two* back-to-back "send control actions"
779+
# bursts for the same UIEvent (~1ms apart; the unified log shows one
780+
# UIEvent delivered to one window, then the doubled sends). Every
781+
# non-idempotent ``on_press`` then fires twice: reducer counters jump
782+
# by two, alerts present twice, pickers reopen after selecting. The
783+
# control action selector therefore takes the ``forEvent:`` argument
784+
# and drops a second send whose UIEvent timestamp matches the last one
785+
# delivered to the same control. Distinct taps (even rapid ones) carry
786+
# distinct event timestamps, and continuous ValueChanged streams
787+
# (slider drags) tick the timestamp per touch move, so only the
788+
# duplicated sends are filtered. Programmatic ``sendActions...`` calls
789+
# pass a nil event and are never deduped.
790+
791+
# Maps control ptr -> UIEvent.timestamp of the last delivered action.
792+
_pn_last_control_event_ts: Dict[int, float] = {}
793+
794+
_objc_msgSend_double = _ct.CFUNCTYPE(_ct.c_double, _ct.c_void_p, _ct.c_void_p)(("objc_msgSend", _libobjc))
795+
796+
_SEL_TIMESTAMP = _sel_reg(b"timestamp")
797+
798+
799+
def _control_action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int, event_ptr: int) -> None:
800+
"""Raw C callback for every PythonNative UIControl event action."""
801+
key = int(sender_ptr or 0)
802+
handler = _pn_action_handlers.get(key)
803+
if handler is None:
804+
return
805+
if event_ptr:
806+
try:
807+
timestamp = float(_objc_msgSend_double(event_ptr, _SEL_TIMESTAMP))
808+
except Exception:
809+
timestamp = None
810+
if timestamp is not None:
811+
if _pn_last_control_event_ts.get(key) == timestamp:
812+
diagnostics.warn_once(
813+
"ios._control_action_imp dropped a duplicated control action "
814+
"(one UIEvent, two sends; a simulator event-injection quirk)",
815+
key=f"control_action_dedupe:{key}",
816+
)
817+
return
818+
_pn_last_control_event_ts[key] = timestamp
819+
try:
820+
handler()
821+
except Exception:
822+
diagnostics.swallowed("ios._control_action_imp")
823+
824+
825+
_control_action_imp_ref = _CONTROL_ACTION_IMP_TYPE(_control_action_imp)
826+
827+
# Gesture recognizers only support zero- or one-argument action
828+
# selectors, so recognizers keep the plain variant while controls use
829+
# the two-argument ``forEvent:`` variant for the dedupe above.
772830
_SEL_ON_ACTION = _sel_reg(b"onPNAction:")
831+
_SEL_ON_CONTROL_ACTION = _sel_reg(b"onPNControlAction:forEvent:")
773832

774833
_PN_ACTION_TARGET_CLS = _alloc_cls(_NS_OBJECT_CLS, b"_PNActionTargetCTypes", 0)
775834
if _PN_ACTION_TARGET_CLS:
@@ -779,6 +838,12 @@ def _action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int) -> None:
779838
_ct.cast(_action_imp_ref, _ct.c_void_p),
780839
b"v@:@",
781840
)
841+
_add_method(
842+
_PN_ACTION_TARGET_CLS,
843+
_SEL_ON_CONTROL_ACTION,
844+
_ct.cast(_control_action_imp_ref, _ct.c_void_p),
845+
b"v@:@@",
846+
)
782847
_reg_cls(_PN_ACTION_TARGET_CLS)
783848

784849
_pn_action_target_ptr: Any = None
@@ -831,7 +896,7 @@ def _register_control_action(control: Any, events_mask: int, handler: Any) -> No
831896
_objc_msgSend.restype = None
832897
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_ulong]
833898
ctl_ptr = control.ptr if hasattr(control, "ptr") else control
834-
_objc_msgSend(ctl_ptr, _SEL_ADD_TARGET_ACTION_EVENTS, target_ptr, _SEL_ON_ACTION, events_mask)
899+
_objc_msgSend(ctl_ptr, _SEL_ADD_TARGET_ACTION_EVENTS, target_ptr, _SEL_ON_CONTROL_ACTION, events_mask)
835900
_pn_action_handlers[_recognizer_ptr(control)] = handler
836901

837902

@@ -1167,6 +1232,7 @@ def destroy(self, native_view: Any) -> None:
11671232
# Controls register their own pointer as the action-handler key
11681233
# (see _register_control_action); drop it with the view.
11691234
_pn_action_handlers.pop(_recognizer_ptr(native_view), None)
1235+
_pn_last_control_event_ts.pop(_recognizer_ptr(native_view), None)
11701236
_pn_view_border_radius_map.pop(id(native_view), None)
11711237
_pn_side_border_map.pop(id(native_view), None)
11721238
try:
@@ -1797,6 +1863,7 @@ def _apply_refresh(self, sv: Any, props: Dict[str, Any]) -> None:
17971863
if existing is not None:
17981864
existing.endRefreshing()
17991865
_pn_action_handlers.pop(_recognizer_ptr(existing), None)
1866+
_pn_last_control_event_ts.pop(_recognizer_ptr(existing), None)
18001867
sv.setRefreshControl_(None)
18011868
sv.setAlwaysBounceVertical_(False)
18021869
except Exception:

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL