Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 51b66c9

Browse files
authored
[Web] Synthesize key events for shift key on pointer events. (#36724)
1 parent c3d4fc9 commit 51b66c9

File tree

4 files changed

+449
-21
lines changed

4 files changed

+449
-21
lines changed

lib/web_ui/lib/src/engine/embedder.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,8 @@ class FlutterViewEmbedder {
323323
_sceneHostElement!.style.opacity = '0.3';
324324
}
325325

326-
PointerBinding.initInstance(glassPaneElement);
327326
KeyboardBinding.initInstance();
327+
PointerBinding.initInstance(glassPaneElement, KeyboardBinding.instance!.converter);
328328

329329
if (domWindow.visualViewport == null && isWebKit) {
330330
// Older Safari versions sometimes give us bogus innerWidth/innerHeight

lib/web_ui/lib/src/engine/keyboard_binding.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:meta/meta.dart';
56
import 'package:ui/ui.dart' as ui;
67

78
import '../engine.dart' show registerHotRestartListener;
@@ -30,6 +31,16 @@ final int _kLogicalShiftLeft = kWebLogicalLocationMap['Shift']![_kLocationLeft]!
3031
final int _kLogicalShiftRight = kWebLogicalLocationMap['Shift']![_kLocationRight]!;
3132
final int _kLogicalMetaLeft = kWebLogicalLocationMap['Meta']![_kLocationLeft]!;
3233
final int _kLogicalMetaRight = kWebLogicalLocationMap['Meta']![_kLocationRight]!;
34+
35+
final int _kPhysicalAltLeft = kWebToPhysicalKey['AltLeft']!;
36+
final int _kPhysicalAltRight = kWebToPhysicalKey['AltRight']!;
37+
final int _kPhysicalControlLeft = kWebToPhysicalKey['ControlLeft']!;
38+
final int _kPhysicalControlRight = kWebToPhysicalKey['ControlRight']!;
39+
final int _kPhysicalShiftLeft = kWebToPhysicalKey['ShiftLeft']!;
40+
final int _kPhysicalShiftRight = kWebToPhysicalKey['ShiftRight']!;
41+
final int _kPhysicalMetaLeft = kWebToPhysicalKey['MetaLeft']!;
42+
final int _kPhysicalMetaRight = kWebToPhysicalKey['MetaRight']!;
43+
3344
// Map logical keys for modifier keys to the functions that can get their
3445
// modifier flag out of an event.
3546
final Map<int, _ModifierGetter> _kLogicalKeyToModifierGetter = <int, _ModifierGetter>{
@@ -106,6 +117,7 @@ class KeyboardBinding {
106117
}
107118
}
108119

120+
KeyboardConverter get converter => _converter;
109121
late final KeyboardConverter _converter;
110122
final Map<String, DomEventListener> _listeners = <String, DomEventListener>{};
111123

@@ -559,4 +571,107 @@ class KeyboardConverter {
559571
_dispatchKeyData = null;
560572
}
561573
}
574+
575+
// Synthesize modifier keys up or down events only when the known pressing states are different.
576+
void synthesizeModifiersIfNeeded(
577+
bool altPressed,
578+
bool controlPressed,
579+
bool metaPressed,
580+
bool shiftPressed,
581+
num eventTimestamp,
582+
) {
583+
_synthesizeModifierIfNeeded(
584+
_kPhysicalAltLeft,
585+
_kPhysicalAltRight,
586+
_kLogicalAltLeft,
587+
_kLogicalAltRight,
588+
altPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
589+
eventTimestamp,
590+
);
591+
_synthesizeModifierIfNeeded(
592+
_kPhysicalControlLeft,
593+
_kPhysicalControlRight,
594+
_kLogicalControlLeft,
595+
_kLogicalControlRight,
596+
controlPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
597+
eventTimestamp,
598+
);
599+
_synthesizeModifierIfNeeded(
600+
_kPhysicalMetaLeft,
601+
_kPhysicalMetaRight,
602+
_kLogicalMetaLeft,
603+
_kLogicalMetaRight,
604+
metaPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
605+
eventTimestamp,
606+
);
607+
_synthesizeModifierIfNeeded(
608+
_kPhysicalShiftLeft,
609+
_kPhysicalShiftRight,
610+
_kLogicalShiftLeft,
611+
_kLogicalShiftRight,
612+
shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
613+
eventTimestamp,
614+
);
615+
}
616+
617+
void _synthesizeModifierIfNeeded(
618+
int physicalLeft,
619+
int physicalRight,
620+
int logicalLeft,
621+
int logicalRight,
622+
ui.KeyEventType type,
623+
num domTimestamp,
624+
) {
625+
final bool leftPressed = _pressingRecords.containsKey(physicalLeft);
626+
final bool rightPressed = _pressingRecords.containsKey(physicalRight);
627+
final bool alreadyPressed = leftPressed || rightPressed;
628+
final bool synthesizeDown = type == ui.KeyEventType.down && !alreadyPressed;
629+
final bool synthesizeUp = type == ui.KeyEventType.up && alreadyPressed;
630+
631+
// Synthesize a down event only for the left key if right and left are not pressed
632+
if (synthesizeDown) {
633+
_synthesizeKeyDownEvent(domTimestamp, physicalLeft, logicalLeft);
634+
}
635+
636+
// Synthesize an up event for left key if pressed
637+
if (synthesizeUp && leftPressed) {
638+
_synthesizeKeyUpEvent(domTimestamp, physicalLeft, logicalLeft);
639+
}
640+
641+
// Synthesize an up event for right key if pressed
642+
if (synthesizeUp && rightPressed) {
643+
_synthesizeKeyUpEvent(domTimestamp, physicalRight, logicalRight);
644+
}
645+
}
646+
647+
void _synthesizeKeyDownEvent(num domTimestamp, int physical, int logical) {
648+
performDispatchKeyData(ui.KeyData(
649+
timeStamp: _eventTimeStampToDuration(domTimestamp),
650+
type: ui.KeyEventType.down,
651+
physical: physical,
652+
logical: logical,
653+
character: null,
654+
synthesized: true,
655+
));
656+
// Update pressing state
657+
_pressingRecords[physical] = logical;
658+
}
659+
660+
void _synthesizeKeyUpEvent(num domTimestamp, int physical, int logical) {
661+
performDispatchKeyData(ui.KeyData(
662+
timeStamp: _eventTimeStampToDuration(domTimestamp),
663+
type: ui.KeyEventType.up,
664+
physical: physical,
665+
logical: logical,
666+
character: null,
667+
synthesized: true,
668+
));
669+
// Update pressing states
670+
_pressingRecords.remove(physical);
671+
}
672+
673+
@visibleForTesting
674+
bool debugKeyIsPressed(int physical) {
675+
return _pressingRecords.containsKey(physical);
676+
}
562677
}

lib/web_ui/lib/src/engine/pointer_binding.dart

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:math' as math;
66

77
import 'package:meta/meta.dart';
8+
import 'package:ui/src/engine/keyboard_binding.dart';
89
import 'package:ui/ui.dart' as ui;
910

1011
import '../engine.dart' show registerHotRestartListener;
@@ -73,7 +74,7 @@ class SafariPointerEventWorkaround {
7374
}
7475

7576
class PointerBinding {
76-
PointerBinding(this.glassPaneElement)
77+
PointerBinding(this.glassPaneElement, this._keyboardConverter)
7778
: _pointerDataConverter = PointerDataConverter(),
7879
_detector = const PointerSupportDetector() {
7980
if (isIosSafari) {
@@ -86,9 +87,9 @@ class PointerBinding {
8687
static PointerBinding? get instance => _instance;
8788
static PointerBinding? _instance;
8889

89-
static void initInstance(DomElement glassPaneElement) {
90+
static void initInstance(DomElement glassPaneElement, KeyboardConverter keyboardConverter) {
9091
if (_instance == null) {
91-
_instance = PointerBinding(glassPaneElement);
92+
_instance = PointerBinding(glassPaneElement, keyboardConverter);
9293
assert(() {
9394
registerHotRestartListener(_instance!.dispose);
9495
return true;
@@ -107,6 +108,7 @@ class PointerBinding {
107108

108109
PointerSupportDetector _detector;
109110
final PointerDataConverter _pointerDataConverter;
111+
KeyboardConverter _keyboardConverter;
110112
late _BaseAdapter _adapter;
111113

112114
/// Should be used in tests to define custom detection of pointer support.
@@ -137,15 +139,23 @@ class PointerBinding {
137139
}
138140
}
139141

142+
@visibleForTesting
143+
void debugOverrideKeyboardConverter(KeyboardConverter keyboardConverter) {
144+
_keyboardConverter = keyboardConverter;
145+
_adapter.clearListeners();
146+
_adapter = _createAdapter();
147+
_pointerDataConverter.clearPointerState();
148+
}
149+
140150
_BaseAdapter _createAdapter() {
141151
if (_detector.hasPointerEvents) {
142-
return _PointerAdapter(_onPointerData, glassPaneElement, _pointerDataConverter);
152+
return _PointerAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter);
143153
}
144154
if (_detector.hasTouchEvents) {
145-
return _TouchAdapter(_onPointerData, glassPaneElement, _pointerDataConverter);
155+
return _TouchAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter);
146156
}
147157
if (_detector.hasMouseEvents) {
148-
return _MouseAdapter(_onPointerData, glassPaneElement, _pointerDataConverter);
158+
return _MouseAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter);
149159
}
150160
throw UnsupportedError('This browser does not support pointer, touch, or mouse events.');
151161
}
@@ -239,14 +249,20 @@ class _Listener {
239249

240250
/// Common functionality that's shared among adapters.
241251
abstract class _BaseAdapter {
242-
_BaseAdapter(this._callback, this.glassPaneElement, this._pointerDataConverter) {
252+
_BaseAdapter(
253+
this._callback,
254+
this.glassPaneElement,
255+
this._pointerDataConverter,
256+
this._keyboardConverter,
257+
) {
243258
setup();
244259
}
245260

246261
final List<_Listener> _listeners = <_Listener>[];
247262
final DomElement glassPaneElement;
248263
final _PointerDataCallback _callback;
249264
final PointerDataConverter _pointerDataConverter;
265+
final KeyboardConverter _keyboardConverter;
250266

251267
/// Each subclass is expected to override this method to attach its own event
252268
/// listeners and convert events into pointer events.
@@ -570,7 +586,8 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
570586
_PointerAdapter(
571587
super.callback,
572588
super.glassPaneElement,
573-
super.pointerDataConverter
589+
super.pointerDataConverter,
590+
super.keyboardConverter,
574591
);
575592

576593
final Map<int, _ButtonSanitizer> _sanitizers = <int, _ButtonSanitizer>{};
@@ -602,13 +619,27 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
602619
String eventName,
603620
_PointerEventListener handler, {
604621
bool useCapture = true,
622+
bool checkModifiers = true,
605623
}) {
606624
addEventListener(target, eventName, (DomEvent event) {
607625
final DomPointerEvent pointerEvent = event as DomPointerEvent;
626+
if (checkModifiers) {
627+
_checkModifiersState(event);
628+
}
608629
handler(pointerEvent);
609630
}, useCapture: useCapture);
610631
}
611632

633+
void _checkModifiersState(DomPointerEvent event) {
634+
_keyboardConverter.synthesizeModifiersIfNeeded(
635+
event.getModifierState('Alt'),
636+
event.getModifierState('Control'),
637+
event.getModifierState('Meta'),
638+
event.getModifierState('Shift'),
639+
event.timeStamp!,
640+
);
641+
}
642+
612643
@override
613644
void setup() {
614645
_addPointerEventListener(glassPaneElement, 'pointerdown', (DomPointerEvent event) {
@@ -654,7 +685,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
654685
_convertEventsToPointerData(data: pointerData, event: event, details: details);
655686
_callback(pointerData);
656687
}
657-
}, useCapture: false);
688+
}, useCapture: false, checkModifiers: false);
658689

659690
_addPointerEventListener(domWindow, 'pointerup', (DomPointerEvent event) {
660691
final int device = _getPointerId(event);
@@ -680,7 +711,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
680711
_convertEventsToPointerData(data: pointerData, event: event, details: details);
681712
_callback(pointerData);
682713
}
683-
});
714+
}, checkModifiers: false);
684715

685716
_addWheelEventListener((DomEvent event) {
686717
_handleWheelEvent(event);
@@ -767,21 +798,35 @@ class _TouchAdapter extends _BaseAdapter {
767798
_TouchAdapter(
768799
super.callback,
769800
super.glassPaneElement,
770-
super.pointerDataConverter
801+
super.pointerDataConverter,
802+
super.keyboardConverter,
771803
);
772804

773805
final Set<int> _pressedTouches = <int>{};
774806
bool _isTouchPressed(int identifier) => _pressedTouches.contains(identifier);
775807
void _pressTouch(int identifier) { _pressedTouches.add(identifier); }
776808
void _unpressTouch(int identifier) { _pressedTouches.remove(identifier); }
777809

778-
void _addTouchEventListener(DomEventTarget target, String eventName, _TouchEventListener handler) {
810+
void _addTouchEventListener(DomEventTarget target, String eventName, _TouchEventListener handler, {bool checkModifiers = true,}) {
779811
addEventListener(target, eventName, (DomEvent event) {
780812
final DomTouchEvent touchEvent = event as DomTouchEvent;
813+
if (checkModifiers) {
814+
_checkModifiersState(event);
815+
}
781816
handler(touchEvent);
782817
});
783818
}
784819

820+
void _checkModifiersState(DomTouchEvent event) {
821+
_keyboardConverter.synthesizeModifiersIfNeeded(
822+
event.altKey,
823+
event.ctrlKey,
824+
event.metaKey,
825+
event.shiftKey,
826+
event.timeStamp!,
827+
);
828+
}
829+
785830
@override
786831
void setup() {
787832
_addTouchEventListener(glassPaneElement, 'touchstart', (DomTouchEvent event) {
@@ -910,7 +955,8 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
910955
_MouseAdapter(
911956
super.callback,
912957
super.glassPaneElement,
913-
super.pointerDataConverter
958+
super.pointerDataConverter,
959+
super.keyboardConverter,
914960
);
915961

916962
final _ButtonSanitizer _sanitizer = _ButtonSanitizer();
@@ -920,13 +966,27 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
920966
String eventName,
921967
_MouseEventListener handler, {
922968
bool useCapture = true,
969+
bool checkModifiers = true,
923970
}) {
924971
addEventListener(target, eventName, (DomEvent event) {
925972
final DomMouseEvent mouseEvent = event as DomMouseEvent;
973+
if (checkModifiers) {
974+
_checkModifiersState(event);
975+
}
926976
handler(mouseEvent);
927977
}, useCapture: useCapture);
928978
}
929979

980+
void _checkModifiersState(DomMouseEvent event) {
981+
_keyboardConverter.synthesizeModifiersIfNeeded(
982+
event.getModifierState('Alt'),
983+
event.getModifierState('Control'),
984+
event.getModifierState('Meta'),
985+
event.getModifierState('Shift'),
986+
event.timeStamp!,
987+
);
988+
}
989+
930990
@override
931991
void setup() {
932992
_addMouseEventListener(glassPaneElement, 'mousedown', (DomMouseEvent event) {

0 commit comments

Comments
 (0)