Skip to content

Commit

Permalink
Switched to using physical keys to support other keyboard models
Browse files Browse the repository at this point in the history
Switched to using `Focus` instead of `KeyboardListener` to properly take focus and make bubbling decisions
Added optional external `foucsNode` & `autofocus` input to `KeyboardOptions`
Documented recommendation to enable arrow keys if WASD enabled (for left handed users)
  • Loading branch information
JaffaKetchup committed Nov 19, 2024
1 parent 83a29cd commit e4a59b9
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 39 deletions.
82 changes: 46 additions & 36 deletions lib/src/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
int _tapUpCounter = 0;
Timer? _doubleTapHoldMaxDelay;

late final _keyboardListenerFocusNode = FocusNode();
late final FocusNode _keyboardListenerFocusNode;
int _keyboardPanEventCounter = 0;
int _keyboardRotateEventCounter = 0;
int _keyboardZoomEventCounter = 0;
final _keyboardPanKeyDownSet = <LogicalKeyboardKey>{};
final _keyboardPanKeyDownSet = <PhysicalKeyboardKey>{};

MapCamera get _camera => widget.controller.camera;

Expand All @@ -118,6 +118,10 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

ServicesBinding.instance.keyboard
.addHandler(cursorKeyboardRotationTriggerHandler);

_keyboardListenerFocusNode =
_interactionOptions.keyboardOptions.focusNode ??
FocusNode(debugLabel: 'FlutterMap');
}

@override
Expand Down Expand Up @@ -295,7 +299,9 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

@override
Widget build(BuildContext context) {
return KeyboardListener(
return Focus(
debugLabel: 'FlutterMap',
autofocus: _interactionOptions.keyboardOptions.autofocus,
focusNode: _keyboardListenerFocusNode,
onKeyEvent: _onKeyEvent,
child: Listener(
Expand Down Expand Up @@ -328,7 +334,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
);
}

void _onKeyEvent(KeyEvent evt) {
KeyEventResult _onKeyEvent(FocusNode _, KeyEvent evt) {
late final arrowKeysGate =
_options.interactionOptions.keyboardOptions.enableArrowKeysPanning
? (evt.logicalKey != LogicalKeyboardKey.arrowLeft &&
Expand All @@ -354,28 +360,30 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
evt.logicalKey != LogicalKeyboardKey.keyF)
: true;

if (arrowKeysGate && wasdKeysGate && qeKeysGate && rfKeysGate) return;
if (arrowKeysGate && wasdKeysGate && qeKeysGate && rfKeysGate) {
return KeyEventResult.ignored;
}

late final arrowKeys =
_options.interactionOptions.keyboardOptions.enableArrowKeysPanning &&
(evt.logicalKey == LogicalKeyboardKey.arrowLeft ||
evt.logicalKey == LogicalKeyboardKey.arrowRight ||
evt.logicalKey == LogicalKeyboardKey.arrowUp ||
evt.logicalKey == LogicalKeyboardKey.arrowDown);
(evt.physicalKey == PhysicalKeyboardKey.arrowLeft ||
evt.physicalKey == PhysicalKeyboardKey.arrowRight ||
evt.physicalKey == PhysicalKeyboardKey.arrowUp ||
evt.physicalKey == PhysicalKeyboardKey.arrowDown);
late final wasdKeys =
_options.interactionOptions.keyboardOptions.enableWASDPanning &&
(evt.logicalKey == LogicalKeyboardKey.keyW ||
evt.logicalKey == LogicalKeyboardKey.keyA ||
evt.logicalKey == LogicalKeyboardKey.keyS ||
evt.logicalKey == LogicalKeyboardKey.keyD);
(evt.physicalKey == PhysicalKeyboardKey.keyW ||
evt.physicalKey == PhysicalKeyboardKey.keyA ||
evt.physicalKey == PhysicalKeyboardKey.keyS ||
evt.physicalKey == PhysicalKeyboardKey.keyD);
late final qeKeys =
_options.interactionOptions.keyboardOptions.enableQERotating &&
(evt.logicalKey == LogicalKeyboardKey.keyQ ||
evt.logicalKey == LogicalKeyboardKey.keyE);
(evt.physicalKey == PhysicalKeyboardKey.keyQ ||
evt.physicalKey == PhysicalKeyboardKey.keyE);
late final rfKeys =
_options.interactionOptions.keyboardOptions.enableRFZooming &&
(evt.logicalKey == LogicalKeyboardKey.keyR ||
evt.logicalKey == LogicalKeyboardKey.keyF);
(evt.physicalKey == PhysicalKeyboardKey.keyR ||
evt.physicalKey == PhysicalKeyboardKey.keyF);

if (evt is KeyDownEvent) {
if (arrowKeys || wasdKeys) {
Expand All @@ -384,24 +392,24 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
_closeFlingAnimationController(MapEventSource.keyboard);
_closeDoubleTapController(MapEventSource.keyboard);
}
_keyboardPanKeyDownSet.add(evt.logicalKey);
}
if (qeKeys) {
_keyboardPanKeyDownSet.add(evt.physicalKey);
} else if (qeKeys) {
_keyboardRotateEventCounter = 0;
_closeFlingAnimationController(MapEventSource.keyboard);
_closeDoubleTapController(MapEventSource.keyboard);
}
if (rfKeys) {
} else if (rfKeys) {
_keyboardZoomEventCounter = 0;
_closeFlingAnimationController(MapEventSource.keyboard);
_closeDoubleTapController(MapEventSource.keyboard);
} else {
return KeyEventResult.skipRemainingHandlers;
}
}
if (evt is KeyUpEvent) {
if (arrowKeys || wasdKeys) {
_keyboardPanKeyDownSet.remove(evt.logicalKey);
_keyboardPanKeyDownSet.remove(evt.physicalKey);
}
return;
return KeyEventResult.skipRemainingHandlers;
}

if (arrowKeys || wasdKeys) _keyboardPanEventCounter++;
Expand All @@ -416,17 +424,17 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
for (final key in _keyboardPanKeyDownSet) {
newCenter = newCenter +
switch (key) {
LogicalKeyboardKey.arrowLeft ||
LogicalKeyboardKey.keyA =>
PhysicalKeyboardKey.arrowLeft ||
PhysicalKeyboardKey.keyA =>
Point(-panSpeed, 0),
LogicalKeyboardKey.arrowRight ||
LogicalKeyboardKey.keyD =>
PhysicalKeyboardKey.arrowRight ||
PhysicalKeyboardKey.keyD =>
Point(panSpeed, 0),
LogicalKeyboardKey.arrowUp ||
LogicalKeyboardKey.keyW =>
PhysicalKeyboardKey.arrowUp ||
PhysicalKeyboardKey.keyW =>
Point(0, -panSpeed),
LogicalKeyboardKey.arrowDown ||
LogicalKeyboardKey.keyS =>
PhysicalKeyboardKey.arrowDown ||
PhysicalKeyboardKey.keyS =>
Point(0, panSpeed),
_ => throw StateError(
'`_keyboardPanKeyDownSet` should only contain arrow & WASD keys',
Expand All @@ -441,10 +449,10 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
_keyboardRotateEventCounter);
var newRotation = _camera.rotation;
if (qeKeys) {
if (evt.logicalKey == LogicalKeyboardKey.keyQ) {
if (evt.physicalKey == PhysicalKeyboardKey.keyQ) {
newRotation -= rotateSpeed;
}
if (evt.logicalKey == LogicalKeyboardKey.keyE) {
if (evt.physicalKey == PhysicalKeyboardKey.keyE) {
newRotation += rotateSpeed;
}
}
Expand All @@ -455,10 +463,10 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
KeyboardOptions.defaultZoomSpeedCalculator(_keyboardZoomEventCounter);
var newZoom = _camera.zoom;
if (rfKeys) {
if (evt.logicalKey == LogicalKeyboardKey.keyR) {
if (evt.physicalKey == PhysicalKeyboardKey.keyR) {
newZoom += zoomSpeed;
}
if (evt.logicalKey == LogicalKeyboardKey.keyF) {
if (evt.physicalKey == PhysicalKeyboardKey.keyF) {
newZoom -= zoomSpeed;
}
}
Expand All @@ -471,6 +479,8 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
hasGesture: true,
source: MapEventSource.keyboard,
);

return KeyEventResult.handled;
}

void _onPointerDown(PointerDownEvent event) {
Expand Down
39 changes: 36 additions & 3 deletions lib/src/map/options/keyboard.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:meta/meta.dart';

Expand All @@ -20,16 +21,31 @@ class KeyboardOptions {
/// This is enabled by default.
final bool enableArrowKeysPanning;

/// Whether to allow the W, A, S, D keys to pan the map (in the directions
/// Whether to allow the W, A, S, D keys (*) to pan the map (in the directions
/// UP, LEFT, DOWN, RIGHT respectively)
///
/// WASD are only the physical and logical keys on QWERTY keyboards. On non-
/// QWERTY keyboards, such as AZERTY, the keys in the same position as on the
/// QWERTY keyboard is used (ie. ZQSD on AZERTY).
///
/// If enabled, it is recommended to enable [enableArrowKeysPanning] to
/// provide panning functionality easily for left handed users.
final bool enableWASDPanning;

/// Whether to allow the Q & E keys to rotate the map (Q rotates COUNTER-
/// Whether to allow the Q & E keys (*) to rotate the map (Q rotates COUNTER-
/// CLOCKWISE, E rotates CLOCKWISE)
///
/// QE are only the physical and logical keys on QWERTY keyboards. On non-
/// QWERTY keyboards, such as AZERTY, the keys in the same position as on the
/// QWERTY keyboard is used (ie. AE on AZERTY).
final bool enableQERotating;

/// Whether to allow the R & F keys to zoom the map (R zooms IN (increases
/// zoom level), F zooms OUT (decreases zoom level))
///
/// RF are only the physical and logical keys on QWERTY keyboards. On non-
/// QWERTY keyboards, such as AZERTY, the keys in the same position as on the
/// QWERTY keyboard is used (ie. RF on AZERTY).
final bool enableRFZooming;

/// Calculates the transformation to apply to the camera's position, where
Expand All @@ -56,6 +72,17 @@ class KeyboardOptions {
/// Defaults to [defaultRotateSpeedCalculator].
final KeyboardEffectSpeedCalculator? rotateSpeedCalculator;

/// Custom [FocusNode] to be used instead of internal node
///
/// May cause unexpected behaviour.
final FocusNode? focusNode;

/// Whether to request focus as soon as the map widget appears (and to enable
/// keyboard controls)
///
/// Defaults to `true`.
final bool autofocus;

/// Create options which specify how the map may be controlled by keyboard
/// keys
///
Expand All @@ -70,13 +97,19 @@ class KeyboardOptions {
this.panSpeedCalculator,
this.zoomSpeedCalculator,
this.rotateSpeedCalculator,
this.focusNode,
this.autofocus = true,
});

/// Disable keyboard control of the map
///
/// [CursorKeyboardRotationOptions] may still be active, and is not disabled
/// if this is disabled.
const KeyboardOptions.disabled() : this(enableArrowKeysPanning: false);
const KeyboardOptions.disabled()
: this(
enableArrowKeysPanning: false,
autofocus: false,
);

/// The default [KeyboardOptions.panSpeedCalculator]
static double defaultPanSpeedCalculator(int counter) => switch (counter) {
Expand Down

0 comments on commit e4a59b9

Please sign in to comment.