| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| part of dart.ui; |
| |
| /// The type of a key event. |
| // Must match the KeyEventType enum in ui/window/key_data.h. |
| enum KeyEventType { |
| /// The key is pressed. |
| down, |
| |
| /// The key is released. |
| up, |
| |
| /// The key is held, causing a repeated key input. |
| repeat; |
| |
| String get label { |
| return switch (this) { |
| down => 'Key Down', |
| up => 'Key Up', |
| repeat => 'Key Repeat', |
| }; |
| } |
| } |
| |
| /// The source device for the key event. |
| /// |
| /// Not all platforms supply an accurate type. |
| // Must match the KeyEventDeviceType enum in ui/window/key_data.h. |
| enum KeyEventDeviceType { |
| /// The device is a keyboard. |
| keyboard, |
| |
| /// The device is a directional pad on something like a television remote |
| /// control or similar. |
| directionalPad, |
| |
| /// The device is a gamepad button |
| gamepad, |
| |
| /// The device is a joystick button |
| joystick, |
| |
| /// The device is a device connected to an HDMI bus. |
| hdmi; |
| |
| String get label { |
| return switch (this) { |
| keyboard => 'Keyboard', |
| directionalPad => 'Directional Pad', |
| gamepad => 'Gamepad', |
| joystick => 'Joystick', |
| hdmi => 'HDMI', |
| }; |
| } |
| } |
| |
| /// Information about a key event. |
| class KeyData { |
| /// Creates an object that represents a key event. |
| const KeyData({ |
| required this.timeStamp, |
| required this.type, |
| required this.physical, |
| required this.logical, |
| required this.character, |
| required this.synthesized, |
| this.deviceType = KeyEventDeviceType.keyboard, |
| }); |
| |
| /// Time of event dispatch, relative to an arbitrary timeline. |
| /// |
| /// For synthesized events, the [timeStamp] might not be the actual time that |
| /// the key press or release happens. |
| final Duration timeStamp; |
| |
| /// The type of the event. |
| final KeyEventType type; |
| |
| /// Describes what type of device (keyboard, directional pad, etc.) this event |
| /// originated from. |
| final KeyEventDeviceType deviceType; |
| |
| /// The key code for the physical key that has changed. |
| final int physical; |
| |
| /// The key code for the logical key that has changed. |
| final int logical; |
| |
| /// Character input from the event. |
| /// |
| /// Ignored for up events. |
| final String? character; |
| |
| /// If [synthesized] is true, this event does not correspond to a native |
| /// event. |
| /// |
| /// Although most of Flutter's keyboard events are transformed from native |
| /// events, some events are not based on native events, and are synthesized |
| /// only to conform Flutter's key event model (as documented in the |
| /// `HardwareKeyboard` class in the framework). |
| /// |
| /// For example, some key downs or ups might be lost when the window loses |
| /// focus. Some platforms provide ways to query whether a key is being held. |
| /// If the embedder detects an inconsistency between its internal record and |
| /// the state returned by the system, the embedder will synthesize a |
| /// corresponding event to synchronize the state without breaking the event |
| /// model. |
| /// |
| /// As another example, macOS treats CapsLock in a special way by sending down |
| /// and up events at the down of alternate presses to indicate the direction |
| /// in which the lock is toggled instead of that the physical key is going. A |
| /// macOS embedder should normalize the behavior by converting a native down |
| /// event into a down event followed immediately by a synthesized up event, |
| /// and the native up event also into a down event followed immediately by a |
| /// synthesized up event. |
| /// |
| /// Synthesized events do not have a trustworthy [timeStamp], and should not |
| /// be processed as if the key actually went down or up at the time of the |
| /// callback. |
| /// |
| /// [KeyRepeatEvent] is never synthesized. |
| final bool synthesized; |
| |
| // Returns the bits that are not included in [valueMask], shifted to the |
| // right. |
| // |
| // For example, if the input is 0x12abcdabcd, then the result is 0x12. |
| // |
| // This is mostly equivalent to a right shift, resolving the problem that |
| // JavaScript only support 32-bit bitwise operations and needs to use division |
| // instead. |
| static int _nonValueBits(int n) { |
| const int valueMask = 0x000FFFFFFFF; |
| // `n >> valueMaskWidth` is equivalent to `n / divisorForValueMask`. |
| const int divisorForValueMask = valueMask + 1; |
| const int valueMaskWidth = 32; |
| |
| // Equivalent to assert(divisorForValueMask == (1 << valueMaskWidth)). |
| const int firstDivisorWidth = 28; |
| assert(divisorForValueMask == |
| (1 << firstDivisorWidth) * (1 << (valueMaskWidth - firstDivisorWidth))); |
| |
| // JS only supports up to 2^53 - 1, therefore non-value bits can only |
| // contain (maxSafeIntegerWidth - valueMaskWidth) bits. |
| const int maxSafeIntegerWidth = 52; |
| const int nonValueMask = (1 << (maxSafeIntegerWidth - valueMaskWidth)) - 1; |
| |
| if (identical(0, 0.0)) { // Detects if we are on the web. |
| return (n / divisorForValueMask).floor() & nonValueMask; |
| } else { |
| return (n >> valueMaskWidth) & nonValueMask; |
| } |
| } |
| |
| String _logicalToString() { |
| final String result = '0x${logical.toRadixString(16)}'; |
| final int planeNum = _nonValueBits(logical) & 0x0FF; |
| final String planeDescription = (() { |
| switch (planeNum) { |
| case 0x000: |
| return ' (Unicode)'; |
| case 0x001: |
| return ' (Unprintable)'; |
| case 0x002: |
| return ' (Flutter)'; |
| case 0x011: |
| return ' (Android)'; |
| case 0x012: |
| return ' (Fuchsia)'; |
| case 0x013: |
| return ' (iOS)'; |
| case 0x014: |
| return ' (macOS)'; |
| case 0x015: |
| return ' (GTK)'; |
| case 0x016: |
| return ' (Windows)'; |
| case 0x017: |
| return ' (Web)'; |
| case 0x018: |
| return ' (GLFW)'; |
| } |
| return ''; |
| })(); |
| return '$result$planeDescription'; |
| } |
| |
| String? _escapeCharacter() { |
| if (character == null) { |
| return '<none>'; |
| } |
| switch (character!) { |
| case '\n': |
| return r'"\n"'; |
| case '\t': |
| return r'"\t"'; |
| case '\r': |
| return r'"\r"'; |
| case '\b': |
| return r'"\b"'; |
| case '\f': |
| return r'"\f"'; |
| default: |
| return '"$character"'; |
| } |
| } |
| |
| String? _quotedCharCode() { |
| if (character == null) { |
| return ''; |
| } |
| final Iterable<String> hexChars = character!.codeUnits |
| .map((int code) => code.toRadixString(16).padLeft(2, '0')); |
| return ' (0x${hexChars.join(' ')})'; |
| } |
| |
| @override |
| String toString() { |
| return 'KeyData(${type.label}, ' |
| 'physical: 0x${physical.toRadixString(16)}, ' |
| 'logical: ${_logicalToString()}, ' |
| 'character: ${_escapeCharacter()}${_quotedCharCode()}' |
| '${synthesized ? ', synthesized' : ''}'; |
| } |
| |
| /// Returns a complete textual description of the information in this object. |
| String toStringFull() { |
| return '$runtimeType(type: ${type.label}, ' |
| 'deviceType: ${deviceType.label}, ' |
| 'timeStamp: $timeStamp, ' |
| 'physical: 0x${physical.toRadixString(16)}, ' |
| 'logical: 0x${logical.toRadixString(16)}, ' |
| 'character: ${_escapeCharacter()}, ' |
| 'synthesized: $synthesized' |
| ')'; |
| } |
| } |