From c727189cc5a6fc563481cbec4ef36126de93541e Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Wed, 12 Oct 2022 14:24:55 +0200 Subject: [PATCH 1/4] Synthesize key events for shift key on pointer events. --- .../lib/src/engine/keyboard_binding.dart | 29 +++++++++++++++++++ .../lib/src/engine/pointer_binding.dart | 13 +++++++++ 2 files changed, 42 insertions(+) diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 74e0fa5f7d050..1f26ee37a3b89 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -152,6 +152,35 @@ class KeyboardBinding { _converter = KeyboardConverter(_onKeyData, onMacOs: operatingSystem == OperatingSystem.macOs); } + // Synthesize shift key up or down event only when the known pressing state is different. + void synthesizeShiftKeyIfNeeded(ui.KeyEventType type) { + // TODO(bleroux): should we take care of shift left AND shift right? + final int physicalShift = kWebToPhysicalKey['ShiftLeft']!; + final bool alreadyPressed = _converter._pressingRecords.containsKey(physicalShift); + final bool synthesizeDown = type == ui.KeyEventType.down && !alreadyPressed; + final bool synthesizeUp = type == ui.KeyEventType.up && alreadyPressed; + if (synthesizeDown || synthesizeUp) { + _converter.performDispatchKeyData(_shiftLeftKeyData(type)); + // Update pressing state + if (synthesizeDown) { + _converter._pressingRecords[physicalShift] = _kLogicalShiftLeft; + } else { + _converter._pressingRecords.remove(physicalShift); + } + } + } + + ui.KeyData _shiftLeftKeyData(ui.KeyEventType type) { + return ui.KeyData( + timeStamp: Duration.zero, + type: type, + physical: kWebToPhysicalKey['ShiftLeft']!, + logical: _kLogicalShiftLeft, + character: null, + synthesized: true, + ); + } + void _reset() { _clearListeners(); _converter.dispose(); diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 75ad7b4387d4d..abd6655372e89 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; +import 'package:ui/src/engine/keyboard_binding.dart'; import 'package:ui/ui.dart' as ui; import '../engine.dart' show registerHotRestartListener; @@ -609,9 +610,19 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }, useCapture: useCapture); } + void _checkModifiersState(DomEvent event) { + // TODO(bleroux): add support for 'Meta', 'Ctrl' and 'Alt' + final DomPointerEvent pointerEvent = event as DomPointerEvent; + final bool shiftPressed = pointerEvent.getModifierState('Shift'); + KeyboardBinding.instance!.synthesizeShiftKeyIfNeeded( + shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + ); + } + @override void setup() { _addPointerEventListener(glassPaneElement, 'pointerdown', (DomPointerEvent event) { + _checkModifiersState(event); final int device = _getPointerId(event); final List pointerData = []; final _ButtonSanitizer sanitizer = _ensureSanitizer(device); @@ -630,6 +641,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }); _addPointerEventListener(domWindow, 'pointermove', (DomPointerEvent event) { + _checkModifiersState(event); final int device = _getPointerId(event); final _ButtonSanitizer sanitizer = _ensureSanitizer(device); final List pointerData = []; @@ -657,6 +669,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }, useCapture: false); _addPointerEventListener(domWindow, 'pointerup', (DomPointerEvent event) { + _checkModifiersState(event); final int device = _getPointerId(event); if (_hasSanitizer(device)) { final List pointerData = []; From a5c151ab0be675a546c254737aeff70cd4fb4070 Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Fri, 14 Oct 2022 11:10:51 +0200 Subject: [PATCH 2/4] Get the timestamp from the DOM event --- lib/web_ui/lib/src/engine/keyboard_binding.dart | 9 +++++---- lib/web_ui/lib/src/engine/pointer_binding.dart | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 1f26ee37a3b89..36a769439346a 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -153,14 +153,15 @@ class KeyboardBinding { } // Synthesize shift key up or down event only when the known pressing state is different. - void synthesizeShiftKeyIfNeeded(ui.KeyEventType type) { + void synthesizeShiftKeyIfNeeded(ui.KeyEventType type, num eventTimestamp) { // TODO(bleroux): should we take care of shift left AND shift right? final int physicalShift = kWebToPhysicalKey['ShiftLeft']!; final bool alreadyPressed = _converter._pressingRecords.containsKey(physicalShift); final bool synthesizeDown = type == ui.KeyEventType.down && !alreadyPressed; final bool synthesizeUp = type == ui.KeyEventType.up && alreadyPressed; if (synthesizeDown || synthesizeUp) { - _converter.performDispatchKeyData(_shiftLeftKeyData(type)); + final Duration timestamp = _eventTimeStampToDuration(eventTimestamp); + _converter.performDispatchKeyData(_shiftLeftKeyData(type, timestamp)); // Update pressing state if (synthesizeDown) { _converter._pressingRecords[physicalShift] = _kLogicalShiftLeft; @@ -170,9 +171,9 @@ class KeyboardBinding { } } - ui.KeyData _shiftLeftKeyData(ui.KeyEventType type) { + ui.KeyData _shiftLeftKeyData(ui.KeyEventType type, Duration timestamp) { return ui.KeyData( - timeStamp: Duration.zero, + timeStamp: timestamp, type: type, physical: kWebToPhysicalKey['ShiftLeft']!, logical: _kLogicalShiftLeft, diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index abd6655372e89..7e63c5960dfea 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -613,9 +613,11 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { void _checkModifiersState(DomEvent event) { // TODO(bleroux): add support for 'Meta', 'Ctrl' and 'Alt' final DomPointerEvent pointerEvent = event as DomPointerEvent; + final bool shiftPressed = pointerEvent.getModifierState('Shift'); KeyboardBinding.instance!.synthesizeShiftKeyIfNeeded( shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + event.timeStamp!, ); } From 7bfc20b32d4f81ea80c7aa97c49e65ac2930abb9 Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Wed, 19 Oct 2022 08:04:19 +0200 Subject: [PATCH 3/4] Check all modifiers for PointerEvent, MouseEvent and TouchEvent --- .../lib/src/engine/keyboard_binding.dart | 120 +++++++++++++++--- .../lib/src/engine/pointer_binding.dart | 53 ++++++-- 2 files changed, 139 insertions(+), 34 deletions(-) diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 36a769439346a..7aff2e7201e78 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -30,6 +30,16 @@ final int _kLogicalShiftLeft = kWebLogicalLocationMap['Shift']![_kLocationLeft]! final int _kLogicalShiftRight = kWebLogicalLocationMap['Shift']![_kLocationRight]!; final int _kLogicalMetaLeft = kWebLogicalLocationMap['Meta']![_kLocationLeft]!; final int _kLogicalMetaRight = kWebLogicalLocationMap['Meta']![_kLocationRight]!; + +final int _kPhysicalAltLeft = kWebToPhysicalKey['AltLeft']!; +final int _kPhysicalAltRight = kWebToPhysicalKey['AltRight']!; +final int _kPhysicalControlLeft = kWebToPhysicalKey['ControlLeft']!; +final int _kPhysicalControlRight = kWebToPhysicalKey['ControlRight']!; +final int _kPhysicalShiftLeft = kWebToPhysicalKey['ShiftLeft']!; +final int _kPhysicalShiftRight = kWebToPhysicalKey['ShiftRight']!; +final int _kPhysicalMetaLeft = kWebToPhysicalKey['MetaLeft']!; +final int _kPhysicalMetaRight = kWebToPhysicalKey['MetaRight']!; + // Map logical keys for modifier keys to the functions that can get their // modifier flag out of an event. final Map _kLogicalKeyToModifierGetter = { @@ -152,34 +162,102 @@ class KeyboardBinding { _converter = KeyboardConverter(_onKeyData, onMacOs: operatingSystem == OperatingSystem.macOs); } - // Synthesize shift key up or down event only when the known pressing state is different. - void synthesizeShiftKeyIfNeeded(ui.KeyEventType type, num eventTimestamp) { - // TODO(bleroux): should we take care of shift left AND shift right? - final int physicalShift = kWebToPhysicalKey['ShiftLeft']!; - final bool alreadyPressed = _converter._pressingRecords.containsKey(physicalShift); + // Synthesize modifier keys up or down events only when the known pressing states are different. + void synthesizeModifiersIfNeeded( + bool altPressed, + bool controlPressed, + bool metaPressed, + bool shiftPressed, + num eventTimestamp, + ) { + _synthesizeModifierIfNeeded( + _kPhysicalAltLeft, + _kPhysicalAltRight, + _kLogicalAltLeft, + _kLogicalAltRight, + altPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + _synthesizeModifierIfNeeded( + _kPhysicalControlLeft, + _kPhysicalControlRight, + _kLogicalControlLeft, + _kLogicalControlRight, + controlPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + _synthesizeModifierIfNeeded( + _kPhysicalMetaLeft, + _kPhysicalMetaRight, + _kLogicalMetaLeft, + _kLogicalMetaRight, + metaPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + _synthesizeModifierIfNeeded( + _kPhysicalShiftLeft, + _kPhysicalShiftRight, + _kLogicalShiftLeft, + _kLogicalShiftRight, + shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + } + + void _synthesizeModifierIfNeeded( + int physicalLeft, + int physicalRight, + int logicalLeft, + int logicalRight, + ui.KeyEventType type, + num domTimestamp, + ) { + final bool leftPressed = _converter._pressingRecords.containsKey(physicalLeft); + final bool rightPressed = _converter._pressingRecords.containsKey(physicalRight); + final bool alreadyPressed = leftPressed || rightPressed; final bool synthesizeDown = type == ui.KeyEventType.down && !alreadyPressed; final bool synthesizeUp = type == ui.KeyEventType.up && alreadyPressed; - if (synthesizeDown || synthesizeUp) { - final Duration timestamp = _eventTimeStampToDuration(eventTimestamp); - _converter.performDispatchKeyData(_shiftLeftKeyData(type, timestamp)); - // Update pressing state - if (synthesizeDown) { - _converter._pressingRecords[physicalShift] = _kLogicalShiftLeft; - } else { - _converter._pressingRecords.remove(physicalShift); - } + + // Synthesize a down event only for the left key if right and left are not pressed + if (synthesizeDown) { + _synthesizeKeyDownEvent(domTimestamp, physicalLeft, logicalLeft); + } + + // Synthesize an up event for left key if pressed + if (synthesizeUp && leftPressed) { + _synthesizeKeyUpEvent(domTimestamp, physicalLeft, logicalLeft); + } + + // Synthesize an up event for right key if pressed + if (synthesizeUp && rightPressed) { + _synthesizeKeyUpEvent(domTimestamp, physicalRight, logicalRight); } } - ui.KeyData _shiftLeftKeyData(ui.KeyEventType type, Duration timestamp) { - return ui.KeyData( - timeStamp: timestamp, - type: type, - physical: kWebToPhysicalKey['ShiftLeft']!, - logical: _kLogicalShiftLeft, + void _synthesizeKeyDownEvent(num domTimestamp, int physical, int logical) { + _converter.performDispatchKeyData(ui.KeyData( + timeStamp: _eventTimeStampToDuration(domTimestamp), + type: ui.KeyEventType.down, + physical: physical, + logical: logical, character: null, synthesized: true, - ); + )); + // Update pressing state + _converter._pressingRecords[physical] = logical; + } + + void _synthesizeKeyUpEvent(num domTimestamp, int physical, int logical) { + _converter.performDispatchKeyData(ui.KeyData( + timeStamp: _eventTimeStampToDuration(domTimestamp), + type: ui.KeyEventType.up, + physical: physical, + logical: logical, + character: null, + synthesized: true, + )); + // Update pressing states + _converter._pressingRecords.remove(physical); } void _reset() { diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 7e63c5960dfea..413b375af3a7a 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -603,20 +603,23 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { String eventName, _PointerEventListener handler, { bool useCapture = true, + bool checkModifiers = true, }) { addEventListener(target, eventName, (DomEvent event) { final DomPointerEvent pointerEvent = event as DomPointerEvent; + if (checkModifiers) { + _checkModifiersState(event); + } handler(pointerEvent); }, useCapture: useCapture); } - void _checkModifiersState(DomEvent event) { - // TODO(bleroux): add support for 'Meta', 'Ctrl' and 'Alt' - final DomPointerEvent pointerEvent = event as DomPointerEvent; - - final bool shiftPressed = pointerEvent.getModifierState('Shift'); - KeyboardBinding.instance!.synthesizeShiftKeyIfNeeded( - shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + void _checkModifiersState(DomPointerEvent event) { + KeyboardBinding.instance!.synthesizeModifiersIfNeeded( + event.getModifierState('Alt'), + event.getModifierState('Control'), + event.getModifierState('Meta'), + event.getModifierState('Shift'), event.timeStamp!, ); } @@ -624,7 +627,6 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { @override void setup() { _addPointerEventListener(glassPaneElement, 'pointerdown', (DomPointerEvent event) { - _checkModifiersState(event); final int device = _getPointerId(event); final List pointerData = []; final _ButtonSanitizer sanitizer = _ensureSanitizer(device); @@ -643,7 +645,6 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }); _addPointerEventListener(domWindow, 'pointermove', (DomPointerEvent event) { - _checkModifiersState(event); final int device = _getPointerId(event); final _ButtonSanitizer sanitizer = _ensureSanitizer(device); final List pointerData = []; @@ -668,10 +669,9 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { _convertEventsToPointerData(data: pointerData, event: event, details: details); _callback(pointerData); } - }, useCapture: false); + }, useCapture: false, checkModifiers: false); _addPointerEventListener(domWindow, 'pointerup', (DomPointerEvent event) { - _checkModifiersState(event); final int device = _getPointerId(event); if (_hasSanitizer(device)) { final List pointerData = []; @@ -695,7 +695,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { _convertEventsToPointerData(data: pointerData, event: event, details: details); _callback(pointerData); } - }); + }, checkModifiers: false); _addWheelEventListener((DomEvent event) { _handleWheelEvent(event); @@ -790,13 +790,26 @@ class _TouchAdapter extends _BaseAdapter { void _pressTouch(int identifier) { _pressedTouches.add(identifier); } void _unpressTouch(int identifier) { _pressedTouches.remove(identifier); } - void _addTouchEventListener(DomEventTarget target, String eventName, _TouchEventListener handler) { + void _addTouchEventListener(DomEventTarget target, String eventName, _TouchEventListener handler, {bool checkModifiers = true,}) { addEventListener(target, eventName, (DomEvent event) { final DomTouchEvent touchEvent = event as DomTouchEvent; + if (checkModifiers) { + _checkModifiersState(event); + } handler(touchEvent); }); } + void _checkModifiersState(DomTouchEvent event) { + KeyboardBinding.instance!.synthesizeModifiersIfNeeded( + event.altKey, + event.ctrlKey, + event.metaKey, + event.shiftKey, + event.timeStamp!, + ); + } + @override void setup() { _addTouchEventListener(glassPaneElement, 'touchstart', (DomTouchEvent event) { @@ -935,13 +948,27 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { String eventName, _MouseEventListener handler, { bool useCapture = true, + bool checkModifiers = true, }) { addEventListener(target, eventName, (DomEvent event) { final DomMouseEvent mouseEvent = event as DomMouseEvent; + if (checkModifiers) { + _checkModifiersState(event); + } handler(mouseEvent); }, useCapture: useCapture); } + void _checkModifiersState(DomMouseEvent event) { + KeyboardBinding.instance!.synthesizeModifiersIfNeeded( + event.getModifierState('Alt'), + event.getModifierState('Control'), + event.getModifierState('Meta'), + event.getModifierState('Shift'), + event.timeStamp!, + ); + } + @override void setup() { _addMouseEventListener(glassPaneElement, 'mousedown', (DomMouseEvent event) { From 14820cc5f83a067b265302b36c82d987128a3227 Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Wed, 19 Oct 2022 16:26:36 +0200 Subject: [PATCH 4/4] Inject KeyboardConverter + add tests --- lib/web_ui/lib/src/engine/embedder.dart | 2 +- .../lib/src/engine/keyboard_binding.dart | 203 ++++++------- .../lib/src/engine/pointer_binding.dart | 44 ++- .../test/engine/pointer_binding_test.dart | 267 +++++++++++++++++- 4 files changed, 397 insertions(+), 119 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 44677dbbefaf9..42479a07c8b2f 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -323,8 +323,8 @@ class FlutterViewEmbedder { _sceneHostElement!.style.opacity = '0.3'; } - PointerBinding.initInstance(glassPaneElement); KeyboardBinding.initInstance(); + PointerBinding.initInstance(glassPaneElement, KeyboardBinding.instance!.converter); if (domWindow.visualViewport == null && isWebKit) { // Older Safari versions sometimes give us bogus innerWidth/innerHeight diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 7aff2e7201e78..bb451771adc0c 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; import '../engine.dart' show registerHotRestartListener; @@ -116,6 +117,7 @@ class KeyboardBinding { } } + KeyboardConverter get converter => _converter; late final KeyboardConverter _converter; final Map _listeners = {}; @@ -162,104 +164,6 @@ class KeyboardBinding { _converter = KeyboardConverter(_onKeyData, onMacOs: operatingSystem == OperatingSystem.macOs); } - // Synthesize modifier keys up or down events only when the known pressing states are different. - void synthesizeModifiersIfNeeded( - bool altPressed, - bool controlPressed, - bool metaPressed, - bool shiftPressed, - num eventTimestamp, - ) { - _synthesizeModifierIfNeeded( - _kPhysicalAltLeft, - _kPhysicalAltRight, - _kLogicalAltLeft, - _kLogicalAltRight, - altPressed ? ui.KeyEventType.down : ui.KeyEventType.up, - eventTimestamp, - ); - _synthesizeModifierIfNeeded( - _kPhysicalControlLeft, - _kPhysicalControlRight, - _kLogicalControlLeft, - _kLogicalControlRight, - controlPressed ? ui.KeyEventType.down : ui.KeyEventType.up, - eventTimestamp, - ); - _synthesizeModifierIfNeeded( - _kPhysicalMetaLeft, - _kPhysicalMetaRight, - _kLogicalMetaLeft, - _kLogicalMetaRight, - metaPressed ? ui.KeyEventType.down : ui.KeyEventType.up, - eventTimestamp, - ); - _synthesizeModifierIfNeeded( - _kPhysicalShiftLeft, - _kPhysicalShiftRight, - _kLogicalShiftLeft, - _kLogicalShiftRight, - shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up, - eventTimestamp, - ); - } - - void _synthesizeModifierIfNeeded( - int physicalLeft, - int physicalRight, - int logicalLeft, - int logicalRight, - ui.KeyEventType type, - num domTimestamp, - ) { - final bool leftPressed = _converter._pressingRecords.containsKey(physicalLeft); - final bool rightPressed = _converter._pressingRecords.containsKey(physicalRight); - final bool alreadyPressed = leftPressed || rightPressed; - final bool synthesizeDown = type == ui.KeyEventType.down && !alreadyPressed; - final bool synthesizeUp = type == ui.KeyEventType.up && alreadyPressed; - - // Synthesize a down event only for the left key if right and left are not pressed - if (synthesizeDown) { - _synthesizeKeyDownEvent(domTimestamp, physicalLeft, logicalLeft); - } - - // Synthesize an up event for left key if pressed - if (synthesizeUp && leftPressed) { - _synthesizeKeyUpEvent(domTimestamp, physicalLeft, logicalLeft); - } - - // Synthesize an up event for right key if pressed - if (synthesizeUp && rightPressed) { - _synthesizeKeyUpEvent(domTimestamp, physicalRight, logicalRight); - } - } - - void _synthesizeKeyDownEvent(num domTimestamp, int physical, int logical) { - _converter.performDispatchKeyData(ui.KeyData( - timeStamp: _eventTimeStampToDuration(domTimestamp), - type: ui.KeyEventType.down, - physical: physical, - logical: logical, - character: null, - synthesized: true, - )); - // Update pressing state - _converter._pressingRecords[physical] = logical; - } - - void _synthesizeKeyUpEvent(num domTimestamp, int physical, int logical) { - _converter.performDispatchKeyData(ui.KeyData( - timeStamp: _eventTimeStampToDuration(domTimestamp), - type: ui.KeyEventType.up, - physical: physical, - logical: logical, - character: null, - synthesized: true, - )); - // Update pressing states - _converter._pressingRecords.remove(physical); - } - void _reset() { _clearListeners(); _converter.dispose(); @@ -667,4 +571,107 @@ class KeyboardConverter { _dispatchKeyData = null; } } + + // Synthesize modifier keys up or down events only when the known pressing states are different. + void synthesizeModifiersIfNeeded( + bool altPressed, + bool controlPressed, + bool metaPressed, + bool shiftPressed, + num eventTimestamp, + ) { + _synthesizeModifierIfNeeded( + _kPhysicalAltLeft, + _kPhysicalAltRight, + _kLogicalAltLeft, + _kLogicalAltRight, + altPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + _synthesizeModifierIfNeeded( + _kPhysicalControlLeft, + _kPhysicalControlRight, + _kLogicalControlLeft, + _kLogicalControlRight, + controlPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + _synthesizeModifierIfNeeded( + _kPhysicalMetaLeft, + _kPhysicalMetaRight, + _kLogicalMetaLeft, + _kLogicalMetaRight, + metaPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + _synthesizeModifierIfNeeded( + _kPhysicalShiftLeft, + _kPhysicalShiftRight, + _kLogicalShiftLeft, + _kLogicalShiftRight, + shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up, + eventTimestamp, + ); + } + + void _synthesizeModifierIfNeeded( + int physicalLeft, + int physicalRight, + int logicalLeft, + int logicalRight, + ui.KeyEventType type, + num domTimestamp, + ) { + final bool leftPressed = _pressingRecords.containsKey(physicalLeft); + final bool rightPressed = _pressingRecords.containsKey(physicalRight); + final bool alreadyPressed = leftPressed || rightPressed; + final bool synthesizeDown = type == ui.KeyEventType.down && !alreadyPressed; + final bool synthesizeUp = type == ui.KeyEventType.up && alreadyPressed; + + // Synthesize a down event only for the left key if right and left are not pressed + if (synthesizeDown) { + _synthesizeKeyDownEvent(domTimestamp, physicalLeft, logicalLeft); + } + + // Synthesize an up event for left key if pressed + if (synthesizeUp && leftPressed) { + _synthesizeKeyUpEvent(domTimestamp, physicalLeft, logicalLeft); + } + + // Synthesize an up event for right key if pressed + if (synthesizeUp && rightPressed) { + _synthesizeKeyUpEvent(domTimestamp, physicalRight, logicalRight); + } + } + + void _synthesizeKeyDownEvent(num domTimestamp, int physical, int logical) { + performDispatchKeyData(ui.KeyData( + timeStamp: _eventTimeStampToDuration(domTimestamp), + type: ui.KeyEventType.down, + physical: physical, + logical: logical, + character: null, + synthesized: true, + )); + // Update pressing state + _pressingRecords[physical] = logical; + } + + void _synthesizeKeyUpEvent(num domTimestamp, int physical, int logical) { + performDispatchKeyData(ui.KeyData( + timeStamp: _eventTimeStampToDuration(domTimestamp), + type: ui.KeyEventType.up, + physical: physical, + logical: logical, + character: null, + synthesized: true, + )); + // Update pressing states + _pressingRecords.remove(physical); + } + + @visibleForTesting + bool debugKeyIsPressed(int physical) { + return _pressingRecords.containsKey(physical); + } } diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 413b375af3a7a..72f646c7c473a 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -74,7 +74,7 @@ class SafariPointerEventWorkaround { } class PointerBinding { - PointerBinding(this.glassPaneElement) + PointerBinding(this.glassPaneElement, this._keyboardConverter) : _pointerDataConverter = PointerDataConverter(), _detector = const PointerSupportDetector() { if (isIosSafari) { @@ -87,9 +87,9 @@ class PointerBinding { static PointerBinding? get instance => _instance; static PointerBinding? _instance; - static void initInstance(DomElement glassPaneElement) { + static void initInstance(DomElement glassPaneElement, KeyboardConverter keyboardConverter) { if (_instance == null) { - _instance = PointerBinding(glassPaneElement); + _instance = PointerBinding(glassPaneElement, keyboardConverter); assert(() { registerHotRestartListener(_instance!.dispose); return true; @@ -108,6 +108,7 @@ class PointerBinding { PointerSupportDetector _detector; final PointerDataConverter _pointerDataConverter; + KeyboardConverter _keyboardConverter; late _BaseAdapter _adapter; /// Should be used in tests to define custom detection of pointer support. @@ -138,15 +139,23 @@ class PointerBinding { } } + @visibleForTesting + void debugOverrideKeyboardConverter(KeyboardConverter keyboardConverter) { + _keyboardConverter = keyboardConverter; + _adapter.clearListeners(); + _adapter = _createAdapter(); + _pointerDataConverter.clearPointerState(); + } + _BaseAdapter _createAdapter() { if (_detector.hasPointerEvents) { - return _PointerAdapter(_onPointerData, glassPaneElement, _pointerDataConverter); + return _PointerAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter); } if (_detector.hasTouchEvents) { - return _TouchAdapter(_onPointerData, glassPaneElement, _pointerDataConverter); + return _TouchAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter); } if (_detector.hasMouseEvents) { - return _MouseAdapter(_onPointerData, glassPaneElement, _pointerDataConverter); + return _MouseAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter); } throw UnsupportedError('This browser does not support pointer, touch, or mouse events.'); } @@ -240,7 +249,12 @@ class _Listener { /// Common functionality that's shared among adapters. abstract class _BaseAdapter { - _BaseAdapter(this._callback, this.glassPaneElement, this._pointerDataConverter) { + _BaseAdapter( + this._callback, + this.glassPaneElement, + this._pointerDataConverter, + this._keyboardConverter, + ) { setup(); } @@ -248,6 +262,7 @@ abstract class _BaseAdapter { final DomElement glassPaneElement; final _PointerDataCallback _callback; final PointerDataConverter _pointerDataConverter; + final KeyboardConverter _keyboardConverter; /// Each subclass is expected to override this method to attach its own event /// listeners and convert events into pointer events. @@ -571,7 +586,8 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { _PointerAdapter( super.callback, super.glassPaneElement, - super.pointerDataConverter + super.pointerDataConverter, + super.keyboardConverter, ); final Map _sanitizers = {}; @@ -615,7 +631,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { } void _checkModifiersState(DomPointerEvent event) { - KeyboardBinding.instance!.synthesizeModifiersIfNeeded( + _keyboardConverter.synthesizeModifiersIfNeeded( event.getModifierState('Alt'), event.getModifierState('Control'), event.getModifierState('Meta'), @@ -782,7 +798,8 @@ class _TouchAdapter extends _BaseAdapter { _TouchAdapter( super.callback, super.glassPaneElement, - super.pointerDataConverter + super.pointerDataConverter, + super.keyboardConverter, ); final Set _pressedTouches = {}; @@ -801,7 +818,7 @@ class _TouchAdapter extends _BaseAdapter { } void _checkModifiersState(DomTouchEvent event) { - KeyboardBinding.instance!.synthesizeModifiersIfNeeded( + _keyboardConverter.synthesizeModifiersIfNeeded( event.altKey, event.ctrlKey, event.metaKey, @@ -938,7 +955,8 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { _MouseAdapter( super.callback, super.glassPaneElement, - super.pointerDataConverter + super.pointerDataConverter, + super.keyboardConverter, ); final _ButtonSanitizer _sanitizer = _ButtonSanitizer(); @@ -960,7 +978,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { } void _checkModifiersState(DomMouseEvent event) { - KeyboardBinding.instance!.synthesizeModifiersIfNeeded( + _keyboardConverter.synthesizeModifiersIfNeeded( event.getModifierState('Alt'), event.getModifierState('Control'), event.getModifierState('Meta'), diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index b713a93e19161..1d3317316a4ab 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -9,6 +9,8 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import '../keyboard_converter_test.dart'; + const int _kNoButtonChange = -1; const PointerSupportDetector _defaultSupportDetector = PointerSupportDetector(); @@ -46,6 +48,13 @@ void testMain() { dpi = window.devicePixelRatio; }); + KeyboardConverter createKeyboardConverter(List keyDataList) { + return KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + } + test('ios workaround', () { debugEmulateIosSafari = true; addTearDown(() { @@ -55,7 +64,9 @@ void testMain() { final MockSafariPointerEventWorkaround mockSafariPointer = MockSafariPointerEventWorkaround(); SafariPointerEventWorkaround.instance = mockSafariPointer; - final PointerBinding instance = PointerBinding(createDomHTMLDivElement()); + final List keyDataList = []; + final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); + final PointerBinding instance = PointerBinding(createDomHTMLDivElement(), keyboardConverter); expect(mockSafariPointer.workAroundInvoked, isIosSafari); instance.dispose(); }, skip: !isSafari); @@ -507,6 +518,248 @@ void testMain() { }, ); + _testEach<_BasicEventContext>( + <_BasicEventContext>[ + _PointerEventContext(), + _MouseEventContext(), + _TouchEventContext(), + ], + 'synthesize modifier keys left down event if left or right are not pressed', + (_BasicEventContext context) { + PointerBinding.instance!.debugOverrideDetector(context); + + // Should synthesize a modifier left key down event when DOM event indicates + // that the modifier key is pressed and known pressing state doesn't contain + // the modifier left key nor the modifier right key. + void shouldSynthesizeLeftDownIfNotPressed(String key) { + final int physicalLeft = kWebToPhysicalKey['${key}Left']!; + final int physicalRight = kWebToPhysicalKey['${key}Right']!; + final int logicalLeft = kWebLogicalLocationMap[key]![kLocationLeft]!; + + final List keyDataList = []; + final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); + PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); + + expect(keyboardConverter.debugKeyIsPressed(physicalLeft), false); + expect(keyboardConverter.debugKeyIsPressed(physicalRight), false); + glassPane.dispatchEvent(context.primaryDown()); + expect(keyDataList.length, 1); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: physicalLeft, + logical: logicalLeft, + character: null, + synthesized: true, + ); + } + + context.altPressed = true; + shouldSynthesizeLeftDownIfNotPressed('Alt'); + context.unpressAllModifiers(); + context.ctrlPressed = true; + shouldSynthesizeLeftDownIfNotPressed('Control'); + context.unpressAllModifiers(); + context.metaPressed = true; + shouldSynthesizeLeftDownIfNotPressed('Meta'); + context.unpressAllModifiers(); + context.shiftPressed = true; + shouldSynthesizeLeftDownIfNotPressed('Shift'); + context.unpressAllModifiers(); + }, + ); + + _testEach<_BasicEventContext>( + <_BasicEventContext>[ + _PointerEventContext(), + _MouseEventContext(), + _TouchEventContext(), + ], + 'should not synthesize modifier keys down event if left or right are pressed', + (_BasicEventContext context) { + PointerBinding.instance!.debugOverrideDetector(context); + + // Should not synthesize a modifier down event when DOM event indicates + // that the modifier key is pressed and known pressing state contains + // the modifier left key. + void shouldNotSynthesizeDownIfLeftPressed(String key, int modifiers) { + final int physicalLeft = kWebToPhysicalKey['${key}Left']!; + final int physicalRight = kWebToPhysicalKey['${key}Right']!; + + final List keyDataList = []; + final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); + PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); + + keyboardConverter.handleEvent(keyDownEvent('${key}Left', key, modifiers, kLocationLeft)); + expect(keyboardConverter.debugKeyIsPressed(physicalLeft), true); + expect(keyboardConverter.debugKeyIsPressed(physicalRight), false); + keyDataList.clear(); // Remove key data generated by handleEvent + + glassPane.dispatchEvent(context.primaryDown()); + expect(keyDataList.length, 0); + } + + // Should not synthesize a modifier down event when DOM event indicates + // that the modifier key is pressed and known pressing state contains + // the modifier right key. + void shouldNotSynthesizeDownIfRightPressed(String key, int modifiers) { + final int physicalLeft = kWebToPhysicalKey['${key}Left']!; + final int physicalRight = kWebToPhysicalKey['${key}Right']!; + + final List keyDataList = []; + final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); + PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); + + keyboardConverter.handleEvent(keyDownEvent('${key}Right', key, modifiers, kLocationRight)); + expect(keyboardConverter.debugKeyIsPressed(physicalLeft), false); + expect(keyboardConverter.debugKeyIsPressed(physicalRight), true); + keyDataList.clear(); // Remove key data generated by handleEvent + + glassPane.dispatchEvent(context.primaryDown()); + expect(keyDataList.length, 0); + } + + context.altPressed = true; + shouldNotSynthesizeDownIfLeftPressed('Alt', kAlt); + shouldNotSynthesizeDownIfRightPressed('Alt', kAlt); + context.unpressAllModifiers(); + context.ctrlPressed = true; + shouldNotSynthesizeDownIfLeftPressed('Control', kCtrl); + shouldNotSynthesizeDownIfRightPressed('Control', kCtrl); + context.unpressAllModifiers(); + context.metaPressed = true; + shouldNotSynthesizeDownIfLeftPressed('Meta', kMeta); + shouldNotSynthesizeDownIfRightPressed('Meta', kMeta); + context.unpressAllModifiers(); + context.shiftPressed = true; + shouldNotSynthesizeDownIfLeftPressed('Shift', kShift); + shouldNotSynthesizeDownIfRightPressed('Shift', kShift); + context.unpressAllModifiers(); + }, + ); + + _testEach<_BasicEventContext>( + <_BasicEventContext>[ + _PointerEventContext(), + _MouseEventContext(), + _TouchEventContext(), + ], + 'synthesize modifier keys up event if left or right are pressed', + (_BasicEventContext context) { + PointerBinding.instance!.debugOverrideDetector(context); + + // Should synthesize a modifier left key up event when DOM event indicates + // that the modifier key is not pressed and known pressing state contains + // the modifier left key. + void shouldSynthesizeLeftUpIfLeftPressed(String key, int modifiers) { + final int physicalLeft = kWebToPhysicalKey['${key}Left']!; + final int physicalRight = kWebToPhysicalKey['${key}Right']!; + final int logicalLeft = kWebLogicalLocationMap[key]![kLocationLeft]!; + + final List keyDataList = []; + final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); + PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); + + keyboardConverter.handleEvent(keyDownEvent('${key}Left', key, modifiers, kLocationLeft)); + expect(keyboardConverter.debugKeyIsPressed(physicalLeft), true); + expect(keyboardConverter.debugKeyIsPressed(physicalRight), false); + keyDataList.clear(); // Remove key data generated by handleEvent + + glassPane.dispatchEvent(context.primaryDown()); + expect(keyDataList.length, 1); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: physicalLeft, + logical: logicalLeft, + character: null, + synthesized: true, + ); + expect(keyboardConverter.debugKeyIsPressed(physicalLeft), false); + } + + // Should synthesize a modifier right key up event when DOM event indicates + // that the modifier key is not pressed and known pressing state contains + // the modifier right key. + void shouldSynthesizeRightUpIfRightPressed(String key, int modifiers) { + final int physicalLeft = kWebToPhysicalKey['${key}Left']!; + final int physicalRight = kWebToPhysicalKey['${key}Right']!; + final int logicalRight = kWebLogicalLocationMap[key]![kLocationRight]!; + + final List keyDataList = []; + final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); + PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); + + keyboardConverter.handleEvent(keyDownEvent('${key}Right', key, modifiers, kLocationRight)); + expect(keyboardConverter.debugKeyIsPressed(physicalLeft), false); + expect(keyboardConverter.debugKeyIsPressed(physicalRight), true); + keyDataList.clear(); // Remove key data generated by handleEvent + + glassPane.dispatchEvent(context.primaryDown()); + expect(keyDataList.length, 1); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: physicalRight, + logical: logicalRight, + character: null, + synthesized: true, + ); + expect(keyboardConverter.debugKeyIsPressed(physicalRight), false); + } + + context.altPressed = false; + shouldSynthesizeLeftUpIfLeftPressed('Alt', kAlt); + shouldSynthesizeRightUpIfRightPressed('Alt', kAlt); + context.ctrlPressed = false; + shouldSynthesizeLeftUpIfLeftPressed('Control', kCtrl); + shouldSynthesizeRightUpIfRightPressed('Control', kCtrl); + context.metaPressed = false; + shouldSynthesizeLeftUpIfLeftPressed('Meta', kMeta); + shouldSynthesizeRightUpIfRightPressed('Meta', kMeta); + context.shiftPressed = false; + shouldSynthesizeLeftUpIfLeftPressed('Shift', kShift); + shouldSynthesizeRightUpIfRightPressed('Shift', kShift); + }, + ); + + _testEach<_BasicEventContext>( + <_BasicEventContext>[ + _PointerEventContext(), + _MouseEventContext(), + _TouchEventContext(), + ], + 'should not synthesize modifier keys up event if left or right are not pressed', + (_BasicEventContext context) { + PointerBinding.instance!.debugOverrideDetector(context); + + // Should not synthesize a modifier up event when DOM event indicates + // that the modifier key is not pressed and known pressing state does + // not contain the modifier left key nor the modifier right key. + void shouldNotSynthesizeUpIfNotPressed(String key) { + final int physicalLeft = kWebToPhysicalKey['${key}Left']!; + final int physicalRight = kWebToPhysicalKey['${key}Right']!; + + final List keyDataList = []; + final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); + PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); + + expect(keyboardConverter.debugKeyIsPressed(physicalLeft), false); + expect(keyboardConverter.debugKeyIsPressed(physicalRight), false); + keyDataList.clear(); // Remove key data generated by handleEvent + + glassPane.dispatchEvent(context.primaryDown()); + expect(keyDataList.length, 0); + } + + context.altPressed = false; + shouldNotSynthesizeUpIfNotPressed('Alt'); + context.ctrlPressed = false; + shouldNotSynthesizeUpIfNotPressed('Control'); + context.metaPressed = false; + shouldNotSynthesizeUpIfNotPressed('Meta'); + context.shiftPressed = false; + shouldNotSynthesizeUpIfNotPressed('Shift'); + }, + ); + _testEach<_ButtonedEventMixin>( <_ButtonedEventMixin>[ if (!isIosSafari) _PointerEventContext(), @@ -2466,7 +2719,7 @@ abstract class _BasicEventContext implements PointerSupportDetector { bool altPressed = false; bool ctrlPressed = false; bool metaPressed = false; - bool shitPressed = false; + bool shiftPressed = false; // Generate an event that is: // @@ -2490,14 +2743,14 @@ abstract class _BasicEventContext implements PointerSupportDetector { altPressed = true; ctrlPressed = true; metaPressed = true; - shitPressed = true; + shiftPressed = true; } void unpressAllModifiers() { altPressed = false; ctrlPressed = false; metaPressed = false; - shitPressed = false; + shiftPressed = false; } } @@ -2687,7 +2940,7 @@ class _TouchEventContext extends _BasicEventContext 'altKey': altPressed, 'ctrlKey': ctrlPressed, 'metaKey': metaPressed, - 'shiftKey': shitPressed, + 'shiftKey': shiftPressed, }, ); } @@ -2826,7 +3079,7 @@ class _MouseEventContext extends _BasicEventContext 'altKey': altPressed, 'ctrlKey': ctrlPressed, 'metaKey': metaPressed, - 'shiftKey': shitPressed, + 'shiftKey': shiftPressed, } ]; return js_util.callConstructor( @@ -2908,7 +3161,7 @@ class _PointerEventContext extends _BasicEventContext 'altKey': altPressed, 'ctrlKey': ctrlPressed, 'metaKey': metaPressed, - 'shiftKey': shitPressed, + 'shiftKey': shiftPressed, }); }