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

[Web] Synthesize key events for modifier keys on pointer events. #36724

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/embedder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions lib/web_ui/lib/src/engine/keyboard_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -30,6 +31,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<int, _ModifierGetter> _kLogicalKeyToModifierGetter = <int, _ModifierGetter>{
Expand Down Expand Up @@ -106,6 +117,7 @@ class KeyboardBinding {
}
}

KeyboardConverter get converter => _converter;
late final KeyboardConverter _converter;
final Map<String, DomEventListener> _listeners = <String, DomEventListener>{};

Expand Down Expand Up @@ -559,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);
}
}
86 changes: 73 additions & 13 deletions lib/web_ui/lib/src/engine/pointer_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,7 +74,7 @@ class SafariPointerEventWorkaround {
}

class PointerBinding {
PointerBinding(this.glassPaneElement)
PointerBinding(this.glassPaneElement, this._keyboardConverter)
: _pointerDataConverter = PointerDataConverter(),
_detector = const PointerSupportDetector() {
if (isIosSafari) {
Expand All @@ -86,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;
Expand All @@ -107,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.
Expand Down Expand Up @@ -137,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.');
}
Expand Down Expand Up @@ -239,14 +249,20 @@ 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();
}

final List<_Listener> _listeners = <_Listener>[];
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.
Expand Down Expand Up @@ -570,7 +586,8 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_PointerAdapter(
super.callback,
super.glassPaneElement,
super.pointerDataConverter
super.pointerDataConverter,
super.keyboardConverter,
);

final Map<int, _ButtonSanitizer> _sanitizers = <int, _ButtonSanitizer>{};
Expand Down Expand Up @@ -602,13 +619,27 @@ 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(DomPointerEvent event) {
_keyboardConverter.synthesizeModifiersIfNeeded(
event.getModifierState('Alt'),
event.getModifierState('Control'),
event.getModifierState('Meta'),
event.getModifierState('Shift'),
event.timeStamp!,
);
}

@override
void setup() {
_addPointerEventListener(glassPaneElement, 'pointerdown', (DomPointerEvent event) {
Expand Down Expand Up @@ -654,7 +685,7 @@ 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) {
final int device = _getPointerId(event);
Expand All @@ -680,7 +711,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_convertEventsToPointerData(data: pointerData, event: event, details: details);
_callback(pointerData);
}
});
}, checkModifiers: false);

_addWheelEventListener((DomEvent event) {
_handleWheelEvent(event);
Expand Down Expand Up @@ -767,21 +798,35 @@ class _TouchAdapter extends _BaseAdapter {
_TouchAdapter(
super.callback,
super.glassPaneElement,
super.pointerDataConverter
super.pointerDataConverter,
super.keyboardConverter,
);

final Set<int> _pressedTouches = <int>{};
bool _isTouchPressed(int identifier) => _pressedTouches.contains(identifier);
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) {
_keyboardConverter.synthesizeModifiersIfNeeded(
event.altKey,
event.ctrlKey,
event.metaKey,
event.shiftKey,
event.timeStamp!,
);
}

@override
void setup() {
_addTouchEventListener(glassPaneElement, 'touchstart', (DomTouchEvent event) {
Expand Down Expand Up @@ -910,7 +955,8 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_MouseAdapter(
super.callback,
super.glassPaneElement,
super.pointerDataConverter
super.pointerDataConverter,
super.keyboardConverter,
);

final _ButtonSanitizer _sanitizer = _ButtonSanitizer();
Expand All @@ -920,13 +966,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) {
_keyboardConverter.synthesizeModifiersIfNeeded(
event.getModifierState('Alt'),
event.getModifierState('Control'),
event.getModifierState('Meta'),
event.getModifierState('Shift'),
event.timeStamp!,
);
}

@override
void setup() {
_addMouseEventListener(glassPaneElement, 'mousedown', (DomMouseEvent event) {
Expand Down
Loading